Snap for 10754184 from 6dedfea7910677def67a3bdfd8bb0bc40202352a to mainline-extservices-release

Change-Id: Ie0bd71fc4619c816e1536dcfe2ac857c4f786be5
diff --git a/.clang-format b/.clang-format
index dce55a8..b384cc6 100644
--- a/.clang-format
+++ b/.clang-format
@@ -11,7 +11,7 @@
 AllowAllParametersOfDeclarationOnNextLine: false
 AllowShortBlocksOnASingleLine: false
 AllowShortCaseLabelsOnASingleLine: false
-AllowShortFunctionsOnASingleLine: InlineOnly
+AllowShortFunctionsOnASingleLine: All
 AllowShortIfStatementsOnASingleLine: false
 AllowShortLoopsOnASingleLine: true
 AlwaysBreakAfterDefinitionReturnType: None
diff --git a/.lgtm.yml b/.github/dependabot.yml
similarity index 83%
copy from .lgtm.yml
copy to .github/dependabot.yml
index 9051e95..b4cb6e3 100644
--- a/.lgtm.yml
+++ b/.github/dependabot.yml
@@ -1,5 +1,5 @@
 #
-#  Copyright (c) 2020, The OpenThread Authors.
+#  Copyright (c) 2022, The OpenThread Authors.
 #  All rights reserved.
 #
 #  Redistribution and use in source and binary forms, with or without
@@ -26,12 +26,13 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-extraction:
-  cpp:
-    prepare:
-      packages:
-        - cmake
-        - ninja-build
-    index:
-      build_command:
-        - THREAD_VERSION=1.2 NODE_MODE=rcp OT_NATIVE_IP=1 ./script/test build
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+    commit-message:
+      prefix: "github-actions"
+    rebase-strategy: "disabled"
+    open-pull-requests-limit: 1
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d158eea..b385b34 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -28,28 +28,38 @@
 
 name: Build
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
 
 jobs:
 
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   pretty:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get --no-install-recommends install -y clang-format-9 clang-tidy-9 shellcheck
+        sudo apt-get --no-install-recommends install -y clang-format-14 clang-tidy-14 shellcheck
         python3 -m pip install yapf==0.31.0
         sudo snap install shfmt
     - name: Check
@@ -59,23 +69,39 @@
   markdown-lint-check:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
-    - uses: gaurav-nelson/github-action-markdown-link-check@v1
+    - 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
+    - uses: gaurav-nelson/github-action-markdown-link-check@5c5dfc0ac2e225883c0e5f03a85311ec2830d368 # v1
       with:
         use-verbose-mode: 'yes'
         max-depth: 3
 
   cmake-version:
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
-        sudo pip3 install --system -U cmake==3.10.3
+        sudo apt-get --no-install-recommends install -y build-essential ninja-build libreadline-dev libncurses-dev
+        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
+        tar xf cmake-3.10.3.tar.gz
+        cd cmake-3.10.3
+        ./configure
+        sudo make install
         cmake --version | grep 3.10.3
-        sudo apt-get --no-install-recommends install -y ninja-build libreadline-dev libncurses-dev
     - name: Build
       run: |
         OT_NODE_TYPE=rcp ./script/test build
@@ -97,7 +123,12 @@
       CC: ${{ matrix.compiler_c }}
       CXX: ${{ matrix.compiler_cpp }}
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -109,33 +140,43 @@
         script/test package
 
   scan-build:
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get --no-install-recommends install -y clang-tools-9 ninja-build
+        sudo apt-get --no-install-recommends install -y clang-tools-14 ninja-build
     - name: Run
       run: |
         script/check-scan-build
 
   mbedtls3-build:
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         sudo apt-get --no-install-recommends install -y ninja-build libreadline-dev libncurses-dev
         rm -rf third_party/mbedtls/repo
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
       with:
         repository: ARMmbed/mbedtls
-        ref: v3.1.0
+        ref: v3.2.1
         path: third_party/mbedtls/repo
     - name: Build
       run: |
@@ -143,7 +184,7 @@
 
   arm-gcc:
     name: arm-gcc-${{ matrix.gcc_ver }}
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     strategy:
       fail-fast: false
       matrix:
@@ -163,37 +204,63 @@
           - 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:
-    - uses: actions/checkout@v2
+    - 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: |
         cd /tmp
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get --no-install-recommends install -y 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
-        # use the minimal required cmake version
-        sudo pip3 install --system -U cmake==3.10.3
+        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 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
+        tar xf cmake-3.10.3.tar.gz
+        cd cmake-3.10.3
+        ./configure
+        sudo make install
         cmake --version | grep 3.10.3
     - name: Build
+      env:
+        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
       run: |
         export PATH=/tmp/${{ matrix.gcc_extract_dir }}/bin:$PATH
         script/check-arm-build
 
   gcc:
     name: gcc-${{ matrix.gcc_ver }}
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-22.04
     strategy:
       fail-fast: false
       matrix:
-        gcc_ver: [5, 6, 7, 8, 9, 10, 11]
+        gcc_ver: [9, 10, 11, 12]
     env:
       CC: gcc-${{ matrix.gcc_ver }}
       CXX: g++-${{ matrix.gcc_ver }}
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -216,12 +283,17 @@
     strategy:
       fail-fast: false
       matrix:
-        clang_ver: ["6.0", "7", "8", "9", "10", "11", "12", "13"]
+        clang_ver: ["9", "10", "11", "12", "13"]
     env:
       CC: clang-${{ matrix.clang_ver }}
       CXX: clang++-${{ matrix.clang_ver }}
     steps:
-      - uses: actions/checkout@v2
+      - 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
@@ -246,7 +318,7 @@
     strategy:
       fail-fast: false
       matrix:
-        clang_ver: ["6.0", "7", "8", "9", "10", "11", "12", "13"]
+        clang_ver: ["9", "10", "11", "12", "13"]
     env:
       CC: clang-${{ matrix.clang_ver }}
       CXX: clang++-${{ matrix.clang_ver }}
@@ -254,7 +326,12 @@
       CXXFLAGS: -m32 -Wconversion
       LDFLAGS: -m32
     steps:
-      - uses: actions/checkout@v2
+      - 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
@@ -277,7 +354,12 @@
   gn:
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -308,27 +390,52 @@
       CC: ${{ matrix.CC }}
       CXX: ${{ matrix.CXX }}
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
-        rm -f '/usr/local/bin/2to3'
         brew update
-        brew install automake m4 ninja
-        [ ${{ matrix.CC }} != clang ] || brew install llvm
+        brew install automake m4
+        wget --tries 4 https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-mac.zip
+        unzip ninja-mac.zip && mv ninja /usr/local/bin/.
     - name: Build
       run: |
         export PATH=$(brew --prefix m4)/bin:$PATH
         script/check-posix-build
         script/check-simulation-build
 
-  android:
-    runs-on: ubuntu-20.04
+  android-ndk:
+    name: android-ndk
+    runs-on: ubuntu-22.04
+    container:
+      image: openthread/environment
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
-        docker run --rm -v $PWD:/build/openthread openthread/android-trusty /build/openthread/script/check-android-build
+        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
new file mode 100644
index 0000000..fc4821b
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,85 @@
+#
+#  Copyright (c) 2022, 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.
+#
+
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ "main" ]
+  pull_request:
+    branches: [ "main" ]
+
+permissions:  # added using https://github.com/step-security/secure-workflows
+  contents: read
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'cpp', 'python' ]
+        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+    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
+
+    - name: Checkout repository
+      uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+
+    - name: Bootstrap
+      run: |
+        sudo apt-get --no-install-recommends install -y ninja-build libreadline-dev libncurses-dev
+
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@7df0ce34898d659f95c0c4a09eaa8d4e32ee64db # v2.2.12
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        
+        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+        # queries: security-extended,security-and-quality
+
+    - run: |
+        ./script/test build
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@7df0ce34898d659f95c0c4a09eaa8d4e32ee64db # v2.2.12
+      with:
+        category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 339bb1a..46786d4 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -28,18 +28,23 @@
 
 name: Docker
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:  # added using https://github.com/step-security/secure-workflows
+  contents: read
 
 jobs:
 
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   buildx:
     name: buildx-${{ matrix.docker_name }}
     runs-on: ubuntu-20.04
@@ -49,7 +54,12 @@
         include:
           - docker_name: environment
     steps:
-    - uses: actions/checkout@v2
+    - 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
 
@@ -63,17 +73,17 @@
 
         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@v1
+      uses: docker/setup-buildx-action@4b4e9c3e2d4531116a6f8ba8e71fc6e2cb6e6c8c # v2.5.0
 
     - name: Docker Buildx (build)
       run: |
@@ -81,7 +91,7 @@
 
     - name: Login to DockerHub
       if: success() && github.repository == 'openthread/openthread' && github.event_name != 'pull_request'
-      uses: docker/login-action@v1
+      uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
       with:
         username: ${{ secrets.DOCKER_USERNAME }}
         password: ${{ secrets.DOCKER_PASSWORD }}
diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml
index 8f549c5..e1b37fe 100644
--- a/.github/workflows/fuzz.yml
+++ b/.github/workflows/fuzz.yml
@@ -27,24 +27,41 @@
 #
 
 name: CIFuzz
-on: [pull_request]
+
+on:
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:  # added using https://github.com/step-security/secure-workflows
+  contents: read
+
 jobs:
  Fuzzing:
    runs-on: ubuntu-20.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
+
    - name: Build Fuzzers
-     uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
+     uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@c0e4bb8d15a68b7f8cc731ea75523e48a2301bcf # master
      with:
        oss-fuzz-project-name: 'openthread'
        dry-run: false
    - name: Run Fuzzers
-     uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
+     uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@c0e4bb8d15a68b7f8cc731ea75523e48a2301bcf # master
      with:
        oss-fuzz-project-name: 'openthread'
        fuzz-seconds: 1800
        dry-run: false
    - name: Upload Crash
-     uses: actions/upload-artifact@v1
+     uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
      if: failure()
      with:
        name: artifacts
diff --git a/.github/workflows/makefile-check.yml b/.github/workflows/makefile-check.yml
index 37e8024..6972074 100644
--- a/.github/workflows/makefile-check.yml
+++ b/.github/workflows/makefile-check.yml
@@ -28,21 +28,31 @@
 
 name: Makefile Check
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:  # added using https://github.com/step-security/secure-workflows
+  contents: read
 
 jobs:
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   makefile-check:
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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: Check
diff --git a/.github/workflows/otbr.yml b/.github/workflows/otbr.yml
index 6f9412b..9cab9ad 100644
--- a/.github/workflows/otbr.yml
+++ b/.github/workflows/otbr.yml
@@ -28,18 +28,23 @@
 
 name: Border Router
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
 
 jobs:
 
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   backbone-router:
     runs-on: ubuntu-20.04
     env:
@@ -57,7 +62,7 @@
       # of OMR prefix and Domain prefix is not deterministic.
       BORDER_ROUTING: 0
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
       with:
         submodules: true
     - name: Build OTBR Docker
@@ -81,11 +86,11 @@
         export CI_ENV="$(bash <(curl -s https://codecov.io/env)) -e GITHUB_ACTIONS -e COVERAGE"
         echo "CI_ENV=${CI_ENV}"
         sudo -E ./script/test cert_suite ./tests/scripts/thread-cert/backbone/*.py || (sudo chmod a+r *.log *.json *.pcap && false)
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-thread-1-3-backbone-docker
         path: /tmp/coverage/
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: thread-1-3-backbone-results
@@ -98,7 +103,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-thread-1-3-backbone
         path: tmp/coverage.info
@@ -138,7 +143,7 @@
             cert_scripts: ./tests/scripts/thread-cert/border_router/nat64/*.py
             packet_verification: 1
             nat64: 1
-            description: "nat64"
+            description: "nat64 openthread"
           - otbr_mdns: "avahi"
             otbr_trel: 0
             cert_scripts: ./tests/scripts/thread-cert/border_router/*.py
@@ -167,7 +172,7 @@
       NAT64: ${{ matrix.nat64 }}
       MAX_JOBS: 3
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
     - name: Build OTBR Docker
       env:
         GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
@@ -190,11 +195,11 @@
         export CI_ENV="$(bash <(curl -s https://codecov.io/env)) -e GITHUB_ACTIONS -e COVERAGE"
         echo "CI_ENV=${CI_ENV}"
         sudo -E ./script/test cert_suite ${{ matrix.cert_scripts }} || (sudo chmod a+r *.log *.json *.pcap && false)
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-thread-border-router-docker
         path: /tmp/coverage/
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: thread-border-router-results
@@ -207,7 +212,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-thread-border-router
         path: tmp/coverage.info
@@ -218,13 +223,13 @@
     - thread-border-router
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
       with:
         submodules: true
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@v2
+    - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
       with:
         path: coverage/
     - name: Combine Coverage
@@ -233,7 +238,7 @@
         script/test combine_coverage
     - name: Upload Coverage
       continue-on-error: true
-      uses: codecov/codecov-action@v2
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
@@ -242,7 +247,7 @@
     needs: upload-coverage
     runs-on: ubuntu-20.04
     steps:
-    - uses: geekyeggo/delete-artifact@1-glob-support
+    - uses: geekyeggo/delete-artifact@54ab544f12cdb7b71613a16a2b5a37a9ade990af # v2.0.0
       with:
         name: cov-*
         useGlob: true
diff --git a/.github/workflows/otci.yml b/.github/workflows/otci.yml
index a1e3a9b..46f0745 100644
--- a/.github/workflows/otci.yml
+++ b/.github/workflows/otci.yml
@@ -28,18 +28,23 @@
 
 name: OTCI
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:  # added using https://github.com/step-security/secure-workflows
+  contents: read
 
 jobs:
 
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   cli-sim:
     name: cli-sim VIRTUAL_TIME=${{ matrix.virtual_time }}
     runs-on: ubuntu-20.04
@@ -48,25 +53,33 @@
       matrix:
         virtual_time: [0, 1]
     env:
-      REFERENCE_DEVICE: 1
       VIRTUAL_TIME: ${{ matrix.virtual_time }}
       REAL_DEVICE: 0
     steps:
-    - uses: actions/checkout@v2
+    - 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
     - 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 46dc33c..ba09fbd 100644
--- a/.github/workflows/otns.yml
+++ b/.github/workflows/otns.yml
@@ -28,7 +28,17 @@
 
 name: OTNS
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
 
 env:
   COVERAGE: 1
@@ -38,28 +48,28 @@
   MAX_NETWORK_SIZE: 999
   GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
 
-jobs:
+permissions:  # added using https://github.com/step-security/secure-workflows
+  contents: read
 
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
+jobs:
 
   unittests:
     name: Unittests
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     steps:
-    - uses: actions/checkout@v2
-    - uses: actions/setup-go@v1
+    - name: Harden Runner
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
-        go-version: '1.14'
-    - name: Set up Python 3.6
-      uses: actions/setup-python@v1
+        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
       with:
-        python-version: 3.6
+        go-version: "1.20"
+    - name: Set up Python
+      uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b # v4.6.0
+      with:
+        python-version: "3.9"
     - name: Bootstrap
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
@@ -73,7 +83,7 @@
           cd /tmp/otns
           ./script/test py-unittests
         )
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: unittests-pcaps
@@ -83,23 +93,23 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-otns-unittests
         path: tmp/coverage.info
 
   examples:
     name: Examples
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-go@v1
+      - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
         with:
-          go-version: '1.14'
-      - name: Set up Python 3.6
-        uses: actions/setup-python@v1
+          go-version: "1.20"
+      - name: Set up Python
+        uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b # v4.6.0
         with:
-          python-version: 3.6
+          python-version: "3.9"
       - name: Bootstrap
         run: |
           sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
@@ -113,7 +123,7 @@
             cd /tmp/otns
             ./script/test py-examples
           )
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
         if: ${{ failure() }}
         with:
           name: examples-pcaps
@@ -123,14 +133,14 @@
       - name: Generate Coverage
         run: |
           ./script/test generate_coverage gcc
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
         with:
           name: cov-otns-examples
           path: tmp/coverage.info
 
   stress-tests:
     name: Stress ${{ matrix.suite }}
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     strategy:
       fail-fast: false
       matrix:
@@ -150,14 +160,19 @@
     env:
       STRESS_LEVEL: ${{ matrix.stress_level }}
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-go@v1
+      - name: Harden Runner
+        uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
         with:
-          go-version: '1.14'
-      - name: Set up Python 3.6
-        uses: actions/setup-python@v1
+          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
         with:
-          python-version: 3.6
+          go-version: "1.20"
+      - name: Set up Python
+        uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b # v4.6.0
+        with:
+          python-version: "3.9"
       - name: Bootstrap
         run: |
           sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
@@ -171,7 +186,7 @@
             cd /tmp/otns
             ./script/test stress-tests ${{ matrix.suite }}
           )
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
         if: ${{ failure() }}
         with:
           name: stress-tests-${{ matrix.suite }}-pcaps
@@ -181,7 +196,7 @@
       - name: Generate Coverage
         run: |
           ./script/test generate_coverage gcc
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
         with:
           name: cov-otns-stress-tests-${{ matrix.suite }}
           path: tmp/coverage.info
@@ -191,13 +206,18 @@
       - unittests
       - examples
       - stress-tests
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@v2
+      - 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
       - name: Bootstrap
         run: |
           sudo apt-get --no-install-recommends install -y lcov
-      - uses: actions/download-artifact@v2
+      - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
         with:
           path: coverage/
       - name: Upload Coverage
diff --git a/.github/workflows/posix.yml b/.github/workflows/posix.yml
index 2ec9fa5..ded9e81 100644
--- a/.github/workflows/posix.yml
+++ b/.github/workflows/posix.yml
@@ -28,25 +28,35 @@
 
 name: POSIX
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
 
 jobs:
 
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   expects-linux:
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     env:
       CFLAGS: -DCLI_COAP_SECURE_USE_COAP_DEFAULT_HANDLER=1 -DOPENTHREAD_CONFIG_MLE_MAX_CHILDREN=15
       CXXFLAGS: -DCLI_COAP_SECURE_USE_COAP_DEFAULT_HANDLER=1 -DOPENTHREAD_CONFIG_MLE_MAX_CHILDREN=15
     steps:
-    - uses: actions/checkout@v2
+    - 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
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y expect ninja-build lcov socat
@@ -55,13 +65,17 @@
         ulimit -c unlimited
         ./script/test prepare_coredump_upload
         OT_OPTIONS='-DOT_READLINE=OFF -DOT_FULL_LOGS=ON -DOT_LOG_OUTPUT=PLATFORM_DEFINED' VIRTUAL_TIME=0 OT_NODE_TYPE=rcp ./script/test build expect
+    - name: Run ot-fct
+      run: |
+        OT_CMAKE_NINJA_TARGET="ot-fct" script/cmake-build posix
+        tests/scripts/expect/ot-fct.exp
     - name: Check Crash
       if: ${{ failure() }}
       run: |
           CRASHED=$(./script/test check_crash | tail -1)
           [[ $CRASHED -eq "1" ]] && echo "Crashed!" || echo "Not crashed."
           echo "CRASHED_RCP=$CRASHED" >> $GITHUB_ENV
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() && env.CRASHED_RCP == '1' }}
       with:
         name: core-expect-rcp
@@ -70,19 +84,19 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-expects-linux-1
         path: tmp/coverage.info
     - name: Run TUN Mode
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get install --no-install-recommends -y dnsmasq bind9-host ntp
-        sudo systemctl start dnsmasq ntp
-        host ipv6.google.com 127.0.0.1
-        echo 'listen-address=::1' | sudo tee /etc/dnsmasq.conf
         echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6
-        sudo systemctl restart dnsmasq
+        sudo apt-get install --no-install-recommends -y bind9-host ntp socat
+        sudo systemctl restart ntp
+        sudo socat 'UDP6-LISTEN:53,fork,reuseaddr,bind=[::1]' UDP:127.0.0.53:53 &
+        socat 'TCP6-LISTEN:2000,fork,reuseaddr' TCP:127.0.0.53:53 &
+        host ipv6.google.com 127.0.0.53
         host ipv6.google.com ::1
         ulimit -c unlimited
         ./script/test prepare_coredump_upload
@@ -93,13 +107,13 @@
           CRASHED=$(./script/test check_crash | tail -1)
           [[ $CRASHED -eq "1" ]] && echo "Crashed!" || echo "Not crashed."
           echo "CRASHED_TUN=$CRASHED" >> $GITHUB_ENV
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() && env.CRASHED_TUN == '1' }}
       with:
         name: core-expect-linux
         path: |
           ./ot-core-dump/*
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: syslog-expect-linux
@@ -107,7 +121,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-expects-linux-2
         path: tmp/coverage.info
@@ -117,37 +131,37 @@
     env:
       COVERAGE: 1
       PYTHONUNBUFFERED: 1
-      READLINE: readline
-      REFERENCE_DEVICE: 1
       THREAD_VERSION: 1.1
       VIRTUAL_TIME: 1
-      VIRTUAL_TIME_UART: 1
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         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
-    - uses: actions/upload-artifact@v2
+        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
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-thread-cert
         path: tmp/coverage.info
@@ -157,7 +171,12 @@
     env:
       COVERAGE: 1
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -174,7 +193,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-ncp-rcp-migrate
         path: tmp/coverage.info
@@ -191,7 +210,12 @@
       OT_DAEMON: ${{ matrix.OT_DAEMON }}
       OT_READLINE: 'readline'
     steps:
-    - uses: actions/checkout@v2
+    - 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
     - name: Bootstrap
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
@@ -219,7 +243,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-pty-linux-${{ matrix.DAEMON }}
         path: tmp/coverage.info
@@ -235,10 +259,24 @@
       OT_DAEMON: ${{ matrix.OT_DAEMON }}
       OT_READLINE: 'off'
     steps:
-    - uses: actions/checkout@v2
+    - 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
     - name: Bootstrap
       run: |
-        rm -f '/usr/local/bin/2to3'
+        rm -f /usr/local/bin/2to3
+        rm -f /usr/local/bin/2to3-3.11
+        rm -f /usr/local/bin/idle3
+        rm -f /usr/local/bin/idle3.11
+        rm -f /usr/local/bin/pydoc3
+        rm -f /usr/local/bin/pydoc3.11
+        rm -f /usr/local/bin/python3
+        rm -f /usr/local/bin/python3.11
+        rm -f /usr/local/bin/python3-config
+        rm -f /usr/local/bin/python3.11-config
         brew update
         brew install ninja socat
     - name: Build
@@ -251,7 +289,12 @@
   rcp-stack-reset:
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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
     - name: Bootstrap
       env:
         GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
@@ -267,7 +310,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-rcp-stack-reset
         path: tmp/coverage.info
@@ -280,20 +323,25 @@
     - thread-cert
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@v2
+    - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
       with:
         path: coverage/
     - name: Combine Coverage
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@v2
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
@@ -302,7 +350,12 @@
     needs: upload-coverage
     runs-on: ubuntu-20.04
     steps:
-    - uses: geekyeggo/delete-artifact@1-glob-support
+    - 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: geekyeggo/delete-artifact@54ab544f12cdb7b71613a16a2b5a37a9ade990af # v2.0.0
       with:
         name: cov-*
         useGlob: true
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
new file mode 100644
index 0000000..3d982a4
--- /dev/null
+++ b/.github/workflows/scorecards.yml
@@ -0,0 +1,100 @@
+#
+#  Copyright (c) 2022, 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.
+#
+
+# This workflow uses actions that are not certified by GitHub. They are provided
+# by a third-party and are governed by separate terms of service, privacy
+# policy, and support documentation.
+
+name: Scorecards supply-chain security
+on:
+  # For Branch-Protection check. Only the default branch is supported. See
+  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
+  branch_protection_rule:
+  # To guarantee Maintained check is occasionally updated. See
+  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
+  schedule:
+    - cron: '33 12 * * 0'
+  push:
+    branches: [ "main" ]
+
+# Declare default permissions as read only.
+permissions: read-all
+
+jobs:
+  analysis:
+    name: Scorecards analysis
+    runs-on: ubuntu-latest
+    permissions:
+      # Needed to upload the results to code-scanning dashboard.
+      security-events: write
+      # Needed to publish results and get a badge (see publish_results below).
+      id-token: write
+      # Uncomment the permissions below if installing in a private repository.
+      # contents: read
+      # actions: read
+
+    steps:
+      - name: "Checkout code"
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+        with:
+          persist-credentials: false
+
+      - name: "Run analysis"
+        uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # v2.1.3
+        with:
+          results_file: results.sarif
+          results_format: sarif
+          # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
+          # - you want to enable the Branch-Protection check on a *public* repository, or
+          # - you are installing Scorecards on a *private* repository
+          # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
+          # repo_token: ${{ secrets.SCORECARD_TOKEN }}
+
+          # Public repositories:
+          #   - Publish results to OpenSSF REST API for easy access by consumers
+          #   - Allows the repository to include the Scorecard badge.
+          #   - See https://github.com/ossf/scorecard-action#publishing-results.
+          # For private repositories:
+          #   - `publish_results` will always be set to `false`, regardless
+          #     of the value entered here.
+          publish_results: true
+
+      # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
+      # format to the repository Actions tab.
+      - name: "Upload artifact"
+        uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.0
+        with:
+          name: SARIF file
+          path: results.sarif
+          retention-days: 5
+
+      # Upload the results to GitHub's code scanning dashboard.
+      - name: "Upload to code-scanning"
+        uses: github/codeql-action/upload-sarif@7df0ce34898d659f95c0c4a09eaa8d4e32ee64db # 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 3754c9d..d996c0e 100644
--- a/.github/workflows/simulation-1.1.yml
+++ b/.github/workflows/simulation-1.1.yml
@@ -28,18 +28,23 @@
 
 name: Simulation 1.1
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
 
 jobs:
 
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   distcheck:
     runs-on: ubuntu-20.04
     env:
@@ -49,7 +54,12 @@
       THREAD_VERSION: 1.1
       VIRTUAL_TIME: 1
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -75,7 +85,12 @@
       VIRTUAL_TIME: 1
       MULTIPLY: 3
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -92,7 +107,7 @@
     - name: Run
       run: |
         ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: packet-verification-pcaps
@@ -102,7 +117,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-packet-verification
         path: tmp/coverage.info
@@ -118,30 +133,34 @@
       THREAD_VERSION: 1.1
       VIRTUAL_TIME: 1
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         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
-    - uses: actions/upload-artifact@v2
+        ./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
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-cli-ftd
         path: tmp/coverage.info
@@ -164,30 +183,34 @@
       VIRTUAL_TIME: 1
       MESSAGE_USE_HEAP: ${{ matrix.message_use_heap }}
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         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
-    - uses: actions/upload-artifact@v2
+        ./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
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-cli-mtd-${{ matrix.message_use_heap }}
         path: tmp/coverage.info
@@ -201,45 +224,53 @@
       COVERAGE: 1
       REFERENCE_DEVICE: 1
       THREAD_VERSION: 1.1
-      TIME_SYNC: 1
       VIRTUAL_TIME: 1
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         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
-    - uses: actions/upload-artifact@v2
+        ./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
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-cli-time-sync
         path: tmp/coverage.info
 
   expects:
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     env:
       CFLAGS: -DCLI_COAP_SECURE_USE_COAP_DEFAULT_HANDLER=1 -DOPENTHREAD_CONFIG_MLE_MAX_CHILDREN=15
       CXXFLAGS: -DCLI_COAP_SECURE_USE_COAP_DEFAULT_HANDLER=1 -DOPENTHREAD_CONFIG_MLE_MAX_CHILDREN=15
       THREAD_VERSION: 1.1
     steps:
-    - uses: actions/checkout@v2
+    - 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
     - name: Bootstrap
       run: |
         sudo apt-get --no-install-recommends install -y expect ninja-build lcov socat
@@ -254,7 +285,7 @@
           CRASHED=$(./script/test check_crash | tail -1)
           [[ $CRASHED -eq "1" ]] && echo "Crashed!" || echo "Not crashed."
           echo "CRASHED_CLI=$CRASHED" >> $GITHUB_ENV
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() && env.CRASHED_CLI == '1' }}
       with:
         name: core-expect-cli
@@ -263,7 +294,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-expects
         path: tmp/coverage.info
@@ -273,7 +304,12 @@
     env:
       THREAD_VERSION: 1.1
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -306,7 +342,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-ot-commissioner
         path: tmp/coverage.info
@@ -315,35 +351,37 @@
     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:
-    - uses: actions/checkout@v2
+    - 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: |
-        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
-    - uses: actions/upload-artifact@v2
+        ./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: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-multiple-instance
         path: tmp/coverage.info
@@ -359,20 +397,25 @@
     - multiple-instance
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@v2
+    - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
       with:
         path: coverage/
     - name: Combine Coverage
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@v2
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
@@ -381,7 +424,12 @@
     needs: upload-coverage
     runs-on: ubuntu-20.04
     steps:
-    - uses: geekyeggo/delete-artifact@1-glob-support
+    - 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: geekyeggo/delete-artifact@54ab544f12cdb7b71613a16a2b5a37a9ade990af # v2.0.0
       with:
         name: cov-*
         useGlob: true
diff --git a/.github/workflows/simulation-1.2.yml b/.github/workflows/simulation-1.2.yml
index 2616646..cbf2c9e 100644
--- a/.github/workflows/simulation-1.2.yml
+++ b/.github/workflows/simulation-1.2.yml
@@ -28,18 +28,23 @@
 
 name: Simulation 1.3
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:  # added using https://github.com/step-security/secure-workflows
+  contents: read
 
 jobs:
 
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   thread-1-3:
     name: thread-1-3-${{ matrix.compiler.c }}-${{ matrix.arch }}
     runs-on: ubuntu-20.04
@@ -51,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:
@@ -59,7 +65,12 @@
         compiler: [{c: "gcc", cxx: "g++", gcov: "gcc"}, { c: "clang-10", cxx: "clang++-10", gcov: "llvm"}]
         arch: ["m32", "m64"]
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -84,12 +95,12 @@
           CRASHED=$(./script/test check_crash | tail -1)
           [[ $CRASHED -eq "1" ]] && echo "Crashed!" || echo "Not crashed."
           echo "CRASHED=$CRASHED" >> $GITHUB_ENV
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: thread-1-3-${{ matrix.compiler.c }}-${{ matrix.arch }}-pcaps
         path: "*.pcap"
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() && env.CRASHED == '1' }}
       with:
         name: core-packet-verification-thread-1-3
@@ -98,7 +109,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage "${{ matrix.compiler.gcov }}"
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-thread-1-3-${{ matrix.compiler.c }}-${{ matrix.arch }}
         path: tmp/coverage.info
@@ -115,7 +126,12 @@
       INTER_OP: 1
       INTER_OP_BBR: 0
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -143,14 +159,14 @@
           CRASHED=$(./script/test check_crash | tail -1)
           [[ $CRASHED -eq "1" ]] && echo "Crashed!" || echo "Not crashed."
           echo "CRASHED=$CRASHED" >> $GITHUB_ENV
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: packet-verification-low-power-pcaps
         path: |
           *.pcap
           *.json
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() && env.CRASHED == '1' }}
       with:
         name: core-packet-verification-low-power
@@ -159,7 +175,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-packet-verification-low-power
         path: tmp/coverage.info
@@ -171,9 +187,15 @@
       VIRTUAL_TIME: 1
       PACKET_VERIFICATION: 1
       THREAD_VERSION: 1.3
+      INTER_OP_BBR: 1
       MULTIPLY: 3
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -190,7 +212,7 @@
     - name: Run
       run: |
         ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: packet-verification-1.1-on-1.3-pcaps
@@ -200,7 +222,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-packet-verification-1-1-on-1-3
         path: tmp/coverage.info
@@ -212,7 +234,12 @@
       THREAD_VERSION: 1.3
       VIRTUAL_TIME: 0
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -229,7 +256,7 @@
           CRASHED=$(./script/test check_crash | tail -1)
           [[ $CRASHED -eq "1" ]] && echo "Crashed!" || echo "Not crashed."
           echo "CRASHED=$CRASHED" >> $GITHUB_ENV
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() && env.CRASHED == '1' }}
       with:
         name: core-expect-1-3
@@ -238,7 +265,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-expects
         path: tmp/coverage.info
@@ -255,7 +282,12 @@
       VIRTUAL_TIME: 1
       INTER_OP: 1
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -282,12 +314,12 @@
           CRASHED=$(./script/test check_crash | tail -1)
           [[ $CRASHED -eq "1" ]] && echo "Crashed!" || echo "Not crashed."
           echo "CRASHED=$CRASHED" >> $GITHUB_ENV
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: thread-1-3-posix-pcaps
         path: "*.pcap"
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() && env.CRASHED == '1' }}
       with:
         name: core-thread-1-3-posix
@@ -296,7 +328,7 @@
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       with:
         name: cov-thread-1-3-posix
         path: tmp/coverage.info
@@ -310,20 +342,25 @@
     - thread-1-3-posix
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@v2
+    - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
       with:
         path: coverage/
     - name: Combine Coverage
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@v2
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
@@ -332,7 +369,12 @@
     needs: upload-coverage
     runs-on: ubuntu-20.04
     steps:
-    - uses: geekyeggo/delete-artifact@1-glob-support
+    - 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: geekyeggo/delete-artifact@54ab544f12cdb7b71613a16a2b5a37a9ade990af # v2.0.0
       with:
         name: cov-*
         useGlob: true
diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml
index 763fb45..71afb0d 100644
--- a/.github/workflows/size.yml
+++ b/.github/workflows/size.yml
@@ -28,22 +28,32 @@
 
 name: Size
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
 
 jobs:
 
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   size-report:
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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
     - name: Bootstrap
       if: "github.event_name == 'push'"
       run: |
diff --git a/.github/workflows/toranj.yml b/.github/workflows/toranj.yml
index 221913f..83cf712 100644
--- a/.github/workflows/toranj.yml
+++ b/.github/workflows/toranj.yml
@@ -28,76 +28,42 @@
 
 name: Toranj
 
-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
 
 jobs:
 
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   toranj-ncp:
     name: toranj-ncp-${{ matrix.TORANJ_RADIO }}
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     strategy:
       fail-fast: false
       matrix:
-        TORANJ_RADIO: ['15.4', 'trel', 'multi']
-    env:
-      COVERAGE: 1
-      TORANJ_RADIO : ${{ matrix.TORANJ_RADIO }}
-    steps:
-    - uses: actions/checkout@v2
-      with:
-        submodules: true
-    - name: Bootstrap
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      run: |
-        sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get --no-install-recommends install -y dbus libdbus-1-dev
-        sudo apt-get --no-install-recommends install -y autoconf-archive
-        sudo apt-get --no-install-recommends install -y bsdtar
-        sudo apt-get --no-install-recommends install -y libtool
-        sudo apt-get --no-install-recommends install -y libglib2.0-dev
-        sudo apt-get --no-install-recommends install -y libboost-dev libboost-signals-dev
-        sudo apt-get --no-install-recommends install -y lcov
-
-        script/git-tool clone --depth=1 --branch=master https://github.com/openthread/wpantund.git
-        cd wpantund
-        ./bootstrap.sh
-        ./configure
-        sudo make -j2
-        sudo make install
-    - name: Build & Run
-      run: |
-        top_builddir=$(pwd)/build/toranj ./tests/toranj/start.sh
-    - name: Generate Coverage
-      if: "matrix.TORANJ_RADIO != 'multi'"
-      run: |
-        ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
-      if: "matrix.TORANJ_RADIO != 'multi'"
-      with:
-        name: cov-toranj-ncp-${{ matrix.TORANJ_RADIO }}
-        path: tmp/coverage.info
-
-  toranj-cli:
-    name: toranj-cli-${{ matrix.TORANJ_RADIO }}
-    runs-on: ubuntu-18.04
-    strategy:
-      matrix:
         TORANJ_RADIO: ['15.4']
     env:
       COVERAGE: 1
       TORANJ_RADIO : ${{ matrix.TORANJ_RADIO }}
-      TORANJ_CLI: 1
+      TORANJ_NCP : 1
+      TORANJ_EVENT_NAME: ${{ github.event_name }}
     steps:
-    - uses: actions/checkout@v2
+    - 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
@@ -105,7 +71,36 @@
         GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get --no-install-recommends install -y lcov
+    - name: Build & Run
+      run: |
+        top_builddir=$(pwd)/build/toranj ./tests/toranj/start.sh
+
+
+  toranj-cli:
+    name: toranj-cli-${{ matrix.TORANJ_RADIO }}
+    runs-on: ubuntu-20.04
+    strategy:
+      matrix:
+        TORANJ_RADIO: ['15.4', 'trel', 'multi']
+    env:
+      COVERAGE: 1
+      TORANJ_RADIO : ${{ matrix.TORANJ_RADIO }}
+      TORANJ_CLI: 1
+    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
+      env:
+        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+      run: |
+        sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
+        sudo apt-get --no-install-recommends install -y ninja-build lcov
         python3 -m pip install -r tests/scripts/thread-cert/requirements.txt
     - name: Build & Run
       run: |
@@ -114,32 +109,62 @@
       if: "matrix.TORANJ_RADIO != 'multi'"
       run: |
         ./script/test generate_coverage gcc
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: "matrix.TORANJ_RADIO != 'multi'"
       with:
         name: cov-toranj-cli-${{ matrix.TORANJ_RADIO }}
         path: tmp/coverage.info
 
+  toranj-unittest:
+    name: toranj-unittest
+    runs-on: ubuntu-20.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
+      env:
+        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+      run: |
+        sudo dpkg --add-architecture i386
+        sudo apt-get update
+        sudo apt-get --no-install-recommends install -y clang-10 clang++-10 ninja-build python3-setuptools python3-wheel llvm lcov
+        sudo apt-get --no-install-recommends install -y g++-multilib libreadline-dev:i386 libncurses-dev:i386
+        python3 -m pip install -r tests/scripts/thread-cert/requirements.txt
+    - name: Build & Run
+      run: |
+        ./tests/toranj/build.sh cmake
+        ninja test
+
   upload-coverage:
     needs:
-    - toranj-ncp
     - toranj-cli
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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: |
         sudo apt-get --no-install-recommends install -y lcov
-    - uses: actions/download-artifact@v2
+    - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
       with:
         path: coverage/
     - name: Combine Coverage
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@v2
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
@@ -148,7 +173,12 @@
     needs: upload-coverage
     runs-on: ubuntu-20.04
     steps:
-    - uses: geekyeggo/delete-artifact@1-glob-support
+    - 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: geekyeggo/delete-artifact@54ab544f12cdb7b71613a16a2b5a37a9ade990af # v2.0.0
       with:
         name: cov-*
         useGlob: true
diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml
new file mode 100644
index 0000000..a92c629
--- /dev/null
+++ b/.github/workflows/unit.yml
@@ -0,0 +1,137 @@
+#
+#  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.
+#
+
+name: Unit
+
+on:
+  push:
+    branches-ignore:
+      - 'dependabot/**'
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:  # added using https://github.com/step-security/secure-workflows
+  contents: read
+
+jobs:
+
+  tcplp-buffering:
+    runs-on: ubuntu-20.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: Build
+      run: make -C third_party/tcplp/lib/test/
+    - name: Run
+      run: third_party/tcplp/lib/test/test_all
+
+  unit-tests:
+    runs-on: ubuntu-20.04
+    env:
+      COVERAGE: 1
+    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: |
+        sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
+        sudo apt-get --no-install-recommends install -y ninja-build lcov
+    - name: Build Simulation
+      run: ./script/cmake-build simulation
+    - name: Test Simulation
+      run: cd build/simulation && ninja test
+    - name: Build POSIX
+      run: ./script/cmake-build posix
+    - name: Test POSIX
+      run: cd build/posix && ninja test
+    - name: Generate Coverage
+      run: |
+        ./script/test generate_coverage gcc
+    - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+      with:
+        name: cov-unit-tests
+        path: tmp/coverage.info
+
+
+  upload-coverage:
+    needs: unit-tests
+    runs-on: ubuntu-20.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: |
+        sudo apt-get --no-install-recommends install -y lcov
+    - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
+      with:
+        path: coverage/
+    - name: Combine Coverage
+      run: |
+        script/test combine_coverage
+    - name: Upload Coverage
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
+      with:
+        files: final.info
+        fail_ci_if_error: true
+
+  delete-coverage-artifacts:
+    needs: upload-coverage
+    runs-on: ubuntu-20.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: geekyeggo/delete-artifact@54ab544f12cdb7b71613a16a2b5a37a9ade990af # v2.0.0
+      with:
+        name: cov-*
+        useGlob: true
diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml
index 37b110c..47828d3 100644
--- a/.github/workflows/version.yml
+++ b/.github/workflows/version.yml
@@ -28,21 +28,28 @@
 
 name: API Version
 
-on: [pull_request]
+on:
+  pull_request:
+    branches:
+      - 'main'
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.repository == 'openthread/openthread' && github.run_id) || github.ref }}
+  cancel-in-progress: true
+
+permissions:  # added using https://github.com/step-security/secure-workflows
+  contents: read
 
 jobs:
-  cancel-previous-runs:
-    runs-on: ubuntu-20.04
-    steps:
-    - uses: rokroskar/workflow-run-cleanup-action@master
-      env:
-        GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
-      if: "github.ref != 'refs/heads/main'"
-
   api-version:
     runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - 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: Check
diff --git a/Android.bp b/Android.bp
index 93f283d..355f00a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -86,6 +86,8 @@
         "-DPACKAGE_URL=\"http://github.com/openthread/openthread\"",
         "-DSPINEL_PLATFORM_HEADER=\"spinel_platform.h\"",
     ],
+    apex_available: [ "com.android.threadnetwork" ],
+    min_sdk_version: "33",
 }
 
 genrule {
@@ -122,59 +124,26 @@
             spi: {
                 cflags: ["-DOPENTHREAD_POSIX_CONFIG_RCP_BUS=OT_POSIX_RCP_BUS_SPI"]
             },
-            hal: {
-                cflags: [
-                    "-DOPENTHREAD_POSIX_CONFIG_RCP_BUS=OT_POSIX_RCP_BUS_VENDOR",
-                ]
-            },
-            conditions_default: {
+            uart: {
                 cflags: [
                     "-DOPENTHREAD_POSIX_CONFIG_RCP_BUS=OT_POSIX_RCP_BUS_UART",
                     "-DOPENTHREAD_POSIX_CONFIG_RCP_PTY_ENABLE=1",
                 ]
             },
+            conditions_default: {
+                cflags: [
+                    "-DOPENTHREAD_POSIX_CONFIG_RCP_BUS=OT_POSIX_RCP_BUS_VENDOR",
+                ]
+            },
         },
     },
+    apex_available: [ "com.android.threadnetwork" ],
+    min_sdk_version: "33",
 }
 
-cc_library_static {
-    name: "ot-core",
-    defaults: ["ot_posix_cflags_defaults", "ot_config_defaults"],
-    generated_headers: ["ot_version_header"],
-
-    local_include_dirs: [
-        "include",
-        "src",
-        "src/android/thread_network_hal",
-        "src/cli",
-        "src/core",
-        "src/ncp",
-        "src/posix/platform",
-        "src/posix/platform/include",
-        "third_party",
-        "third_party/mbedtls",
-        "third_party/mbedtls/repo/include",
-    ],
-
-    export_include_dirs: [
-        "include",
-        "src",
-    ],
-
-    cppflags: [
-        "-pedantic-errors",
-        "-Wno-non-virtual-dtor",
-    ],
-
-    shared_libs: [
-        "libbase",
-        "libcutils",
-        "libutils",
-    ],
-
+filegroup {
+    name: "openthread_core_srcs",
     srcs: [
-        "src/android/thread_network_hal/hal_interface.cpp",
-        "src/android/thread_network_hal/vendor_interface.cpp",
         "src/core/api/backbone_router_api.cpp",
         "src/core/api/backbone_router_ftd_api.cpp",
         "src/core/api/border_agent_api.cpp",
@@ -195,6 +164,7 @@
         "src/core/api/dns_server_api.cpp",
         "src/core/api/error_api.cpp",
         "src/core/api/heap_api.cpp",
+        "src/core/api/history_tracker_api.cpp",
         "src/core/api/icmp6_api.cpp",
         "src/core/api/instance_api.cpp",
         "src/core/api/ip6_api.cpp",
@@ -204,8 +174,10 @@
         "src/core/api/link_metrics_api.cpp",
         "src/core/api/link_raw_api.cpp",
         "src/core/api/logging_api.cpp",
+        "src/core/api/mesh_diag_api.cpp",
         "src/core/api/message_api.cpp",
         "src/core/api/multi_radio_api.cpp",
+        "src/core/api/nat64_api.cpp",
         "src/core/api/netdata_api.cpp",
         "src/core/api/netdata_publisher_api.cpp",
         "src/core/api/netdiag_api.cpp",
@@ -220,8 +192,10 @@
         "src/core/api/srp_server_api.cpp",
         "src/core/api/tasklet_api.cpp",
         "src/core/api/tcp_api.cpp",
+        "src/core/api/tcp_ext_api.cpp",
         "src/core/api/thread_api.cpp",
         "src/core/api/thread_ftd_api.cpp",
+        "src/core/api/trel_api.cpp",
         "src/core/api/udp_api.cpp",
         "src/core/backbone_router/backbone_tmf.cpp",
         "src/core/backbone_router/bbr_leader.cpp",
@@ -234,6 +208,7 @@
         "src/core/coap/coap.cpp",
         "src/core/coap/coap_message.cpp",
         "src/core/coap/coap_secure.cpp",
+        "src/core/common/appender.cpp",
         "src/core/common/binary_search.cpp",
         "src/core/common/crc16.cpp",
         "src/core/common/data.cpp",
@@ -247,6 +222,7 @@
         "src/core/common/log.cpp",
         "src/core/common/message.cpp",
         "src/core/common/notifier.cpp",
+        "src/core/common/preference.cpp",
         "src/core/common/random.cpp",
         "src/core/common/settings.cpp",
         "src/core/common/string.cpp",
@@ -255,14 +231,13 @@
         "src/core/common/timer.cpp",
         "src/core/common/tlvs.cpp",
         "src/core/common/trickle_timer.cpp",
+        "src/core/common/uptime.cpp",
         "src/core/crypto/aes_ccm.cpp",
         "src/core/crypto/aes_ecb.cpp",
         "src/core/crypto/crypto_platform.cpp",
-        "src/core/crypto/ecdsa.cpp",
         "src/core/crypto/hkdf_sha256.cpp",
         "src/core/crypto/hmac_sha256.cpp",
         "src/core/crypto/mbedtls.cpp",
-        "src/core/crypto/pbkdf2_cmac.cpp",
         "src/core/crypto/sha256.cpp",
         "src/core/crypto/storage.cpp",
         "src/core/diags/factory_diags.cpp",
@@ -300,6 +275,8 @@
         "src/core/net/dhcp6_client.cpp",
         "src/core/net/dhcp6_server.cpp",
         "src/core/net/dns_client.cpp",
+        "src/core/net/dns_dso.cpp",
+        "src/core/net/dns_platform.cpp",
         "src/core/net/dns_types.cpp",
         "src/core/net/dnssd_server.cpp",
         "src/core/net/icmp6.cpp",
@@ -309,6 +286,7 @@
         "src/core/net/ip6_filter.cpp",
         "src/core/net/ip6_headers.cpp",
         "src/core/net/ip6_mpl.cpp",
+        "src/core/net/nat64_translator.cpp",
         "src/core/net/nd6.cpp",
         "src/core/net/nd_agent.cpp",
         "src/core/net/netif.cpp",
@@ -317,6 +295,7 @@
         "src/core/net/srp_client.cpp",
         "src/core/net/srp_server.cpp",
         "src/core/net/tcp6.cpp",
+        "src/core/net/tcp6_ext.cpp",
         "src/core/net/udp6.cpp",
         "src/core/radio/radio.cpp",
         "src/core/radio/radio_callbacks.cpp",
@@ -327,6 +306,8 @@
         "src/core/thread/address_resolver.cpp",
         "src/core/thread/announce_begin_server.cpp",
         "src/core/thread/announce_sender.cpp",
+        "src/core/thread/anycast_locator.cpp",
+        "src/core/thread/child_supervision.cpp",
         "src/core/thread/child_table.cpp",
         "src/core/thread/csl_tx_scheduler.cpp",
         "src/core/thread/discover_scanner.cpp",
@@ -335,6 +316,7 @@
         "src/core/thread/indirect_sender.cpp",
         "src/core/thread/key_manager.cpp",
         "src/core/thread/link_metrics.cpp",
+        "src/core/thread/link_metrics_types.cpp",
         "src/core/thread/link_quality.cpp",
         "src/core/thread/lowpan.cpp",
         "src/core/thread/mesh_forwarder.cpp",
@@ -342,6 +324,7 @@
         "src/core/thread/mesh_forwarder_mtd.cpp",
         "src/core/thread/mle.cpp",
         "src/core/thread/mle_router.cpp",
+        "src/core/thread/mle_tlvs.cpp",
         "src/core/thread/mle_types.cpp",
         "src/core/thread/mlr_manager.cpp",
         "src/core/thread/neighbor_table.cpp",
@@ -366,41 +349,77 @@
         "src/core/thread/uri_paths.cpp",
         "src/core/utils/channel_manager.cpp",
         "src/core/utils/channel_monitor.cpp",
-        "src/core/utils/child_supervision.cpp",
         "src/core/utils/flash.cpp",
         "src/core/utils/heap.cpp",
+        "src/core/utils/history_tracker.cpp",
         "src/core/utils/jam_detector.cpp",
+        "src/core/utils/mesh_diag.cpp",
         "src/core/utils/otns.cpp",
         "src/core/utils/parse_cmdline.cpp",
         "src/core/utils/ping_sender.cpp",
+        "src/core/utils/power_calibration.cpp",
         "src/core/utils/slaac_address.cpp",
         "src/core/utils/srp_client_buffers.cpp",
-        "src/lib/hdlc/hdlc.cpp",
-        "src/lib/platform/exit_code.c",
-        "src/lib/spinel/spinel.c",
-        "src/lib/spinel/spinel_buffer.cpp",
-        "src/lib/spinel/spinel_decoder.cpp",
-        "src/lib/spinel/spinel_encoder.cpp",
-        "src/lib/url/url.cpp",
-        "src/posix/platform/alarm.cpp",
-        "src/posix/platform/backbone.cpp",
-        "src/posix/platform/daemon.cpp",
-        "src/posix/platform/entropy.cpp",
-        "src/posix/platform/hdlc_interface.cpp",
-        "src/posix/platform/infra_if.cpp",
-        "src/posix/platform/logging.cpp",
-        "src/posix/platform/mainloop.cpp",
-        "src/posix/platform/memory.cpp",
-        "src/posix/platform/misc.cpp",
-        "src/posix/platform/multicast_routing.cpp",
-        "src/posix/platform/netif.cpp",
-        "src/posix/platform/radio.cpp",
-        "src/posix/platform/radio_url.cpp",
-        "src/posix/platform/settings.cpp",
-        "src/posix/platform/spi_interface.cpp",
-        "src/posix/platform/system.cpp",
-        "src/posix/platform/trel.cpp",
-        "src/posix/platform/udp.cpp",
+    ],
+}
+
+filegroup {
+    name: "openthread_cli_srcs",
+    srcs: [
+        "src/cli/cli.cpp",
+        "src/cli/cli_br.cpp",
+        "src/cli/cli_coap.cpp",
+        "src/cli/cli_coap_secure.cpp",
+        "src/cli/cli_commissioner.cpp",
+        "src/cli/cli_dataset.cpp",
+        "src/cli/cli_history.cpp",
+        "src/cli/cli_joiner.cpp",
+        "src/cli/cli_network_data.cpp",
+        "src/cli/cli_output.cpp",
+        "src/cli/cli_srp_client.cpp",
+        "src/cli/cli_srp_server.cpp",
+        "src/cli/cli_tcp.cpp",
+        "src/cli/cli_udp.cpp",
+    ],
+}
+
+filegroup {
+    name: "openthread_ncp_srcs",
+    srcs: [
+        "src/ncp/changed_props_set.cpp",
+        "src/ncp/ncp_base.cpp",
+        "src/ncp/ncp_base_dispatcher.cpp",
+        "src/ncp/ncp_base_radio.cpp",
+        "src/ncp/ncp_spi.cpp",
+        "src/ncp/ncp_hdlc.cpp",
+    ],
+}
+
+filegroup {
+    name: "openthread_simulation_srcs",
+    srcs: [
+        "examples/platforms/simulation/alarm.c",
+        "examples/platforms/simulation/crypto.c",
+        "examples/platforms/simulation/diag.c",
+        "examples/platforms/simulation/entropy.c",
+        "examples/platforms/simulation/flash.c",
+        "examples/platforms/simulation/infra_if.c",
+        "examples/platforms/simulation/logging.c",
+        "examples/platforms/simulation/misc.c",
+        "examples/platforms/simulation/radio.c",
+        "examples/platforms/simulation/spi-stubs.c",
+        "examples/platforms/simulation/system.c",
+        "examples/platforms/simulation/trel.c",
+        "examples/platforms/simulation/uart.c",
+        "examples/platforms/utils/link_metrics.cpp",
+        "examples/platforms/utils/mac_frame.cpp",
+        "examples/platforms/utils/soft_source_match_table.c",
+    ],
+}
+
+filegroup {
+    name: "openthread_mbedtls_srcs",
+    srcs: [
         "third_party/mbedtls/repo/library/aes.c",
         "third_party/mbedtls/repo/library/asn1parse.c",
         "third_party/mbedtls/repo/library/asn1write.c",
@@ -445,39 +464,104 @@
     ],
 }
 
+filegroup {
+    name: "openthread_platform_posix_srcs",
+    srcs: [
+        "src/posix/platform/alarm.cpp",
+        "src/posix/platform/backbone.cpp",
+        "src/posix/platform/backtrace.cpp",
+        "src/posix/platform/config_file.cpp",
+        "src/posix/platform/daemon.cpp",
+        "src/posix/platform/entropy.cpp",
+        "src/posix/platform/firewall.cpp",
+        "src/posix/platform/hdlc_interface.cpp",
+        "src/posix/platform/infra_if.cpp",
+        "src/posix/platform/logging.cpp",
+        "src/posix/platform/mainloop.cpp",
+        "src/posix/platform/memory.cpp",
+        "src/posix/platform/misc.cpp",
+        "src/posix/platform/multicast_routing.cpp",
+        "src/posix/platform/netif.cpp",
+        "src/posix/platform/power.cpp",
+        "src/posix/platform/power_updater.cpp",
+        "src/posix/platform/radio.cpp",
+        "src/posix/platform/radio_url.cpp",
+        "src/posix/platform/resolver.cpp",
+        "src/posix/platform/settings.cpp",
+        "src/posix/platform/spi_interface.cpp",
+        "src/posix/platform/system.cpp",
+        "src/posix/platform/trel.cpp",
+        "src/posix/platform/udp.cpp",
+        "src/posix/platform/utils.cpp",
+    ],
+}
+
 cc_library_static {
-    name: "openthread-tcplp",
+    name: "ot-core",
+    defaults: [
+        "ot_config_defaults",
+        "ot-daemon-release-cc-defaults",
+        "ot_posix_cflags_defaults",
+    ],
+    generated_headers: ["ot_version_header"],
 
     local_include_dirs: [
         "include",
+        "src",
+        "src/android/thread_network_hal",
+        "src/cli",
+        "src/core",
+        "src/ncp",
+        "src/posix/platform",
+        "src/posix/platform/include",
+        "third_party",
+        "third_party/mbedtls",
+        "third_party/mbedtls/repo/include",
     ],
 
-    cflags: [
-        "-Wno-sign-compare",
-        "-Wno-conversion",
-        "-Wno-unused-parameter",
+    export_include_dirs: [
+        "include",
+        "src",
+    ],
+
+    cppflags: [
+        "-pedantic-errors",
+        "-Wno-non-virtual-dtor",
+    ],
+
+    vintf_fragments: ["src/android/thread_network_hal/device_manifest.xml"],
+    shared_libs: [
+        "libbase",
+        "libcutils",
+        "libutils",
+        "libbinder_ndk",
+        "android.hardware.threadnetwork-V1-ndk",
     ],
 
     srcs: [
-        "third_party/tcplp/bsdtcp/cc/cc_newreno.c",
-        "third_party/tcplp/bsdtcp/tcp_input.c",
-        "third_party/tcplp/bsdtcp/tcp_output.c",
-        "third_party/tcplp/bsdtcp/tcp_reass.c",
-        "third_party/tcplp/bsdtcp/tcp_sack.c",
-        "third_party/tcplp/bsdtcp/tcp_subr.c",
-        "third_party/tcplp/bsdtcp/tcp_timer.c",
-        "third_party/tcplp/bsdtcp/tcp_timewait.c",
-        "third_party/tcplp/bsdtcp/tcp_usrreq.c",
-        "third_party/tcplp/lib/bitmap.c",
-        "third_party/tcplp/lib/cbuf.c",
-        "third_party/tcplp/lib/lbuf.c",
+        ":openthread_core_srcs",
+        ":openthread_mbedtls_srcs",
+        ":openthread_platform_posix_srcs",
+        "src/android/thread_network_hal/hal_interface.cpp",
+        "src/android/thread_network_hal/vendor_interface.cpp",
+        "src/lib/hdlc/hdlc.cpp",
+        "src/lib/platform/exit_code.c",
+        "src/lib/spinel/spinel.c",
+        "src/lib/spinel/spinel_buffer.cpp",
+        "src/lib/spinel/spinel_decoder.cpp",
+        "src/lib/spinel/spinel_encoder.cpp",
+        "src/lib/url/url.cpp",
     ],
 }
 
 cc_library_static {
     name: "libopenthread-cli",
 
-    defaults: ["ot_posix_cflags_defaults", "ot_config_defaults"],
+    defaults: [
+        "ot_config_defaults",
+        "ot-daemon-release-cc-defaults",
+        "ot_posix_cflags_defaults",
+    ],
     generated_headers: ["ot_version_header"],
 
     local_include_dirs: [
@@ -504,6 +588,7 @@
 
     srcs: [
         "src/cli/cli.cpp",
+        "src/cli/cli_br.cpp",
         "src/cli/cli_coap.cpp",
         "src/cli/cli_coap_secure.cpp",
         "src/cli/cli_commissioner.cpp",
@@ -576,7 +661,9 @@
         "-DOPENTHREAD_CONFIG_LOG_LEVEL_DYNAMIC_ENABLE=1",
         "-DOPENTHREAD_CONFIG_MAC_FILTER_ENABLE=1",
         "-DOPENTHREAD_CONFIG_NCP_HDLC_ENABLE=1",
+        "-DOPENTHREAD_CONFIG_PING_SENDER_ENABLE=1",
         "-DOPENTHREAD_EXAMPLES_SIMULATION=1",
+        "-DOPENTHREAD_CONFIG_TCP_ENABLE=0",
         "-DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"examples/platforms/simulation/openthread-core-simulation-config.h\"",
         "-DPACKAGE=\"openthread\"",
         "-DPACKAGE_BUGREPORT=\"openthread-devel@googlegroups.com\"",
@@ -589,7 +676,7 @@
 
 cc_library_static {
     name: "openthread-simulation",
-    vendor: true,
+    vendor_available: true,
 
     defaults: [
         "ot_rcp_cflags_defaults",
@@ -597,22 +684,7 @@
     ],
 
     srcs: [
-        "examples/platforms/simulation/alarm.c",
-        "examples/platforms/simulation/crypto.c",
-        "examples/platforms/simulation/diag.c",
-        "examples/platforms/simulation/entropy.c",
-        "examples/platforms/simulation/flash.c",
-        "examples/platforms/simulation/infra_if.c",
-        "examples/platforms/simulation/logging.c",
-        "examples/platforms/simulation/misc.c",
-        "examples/platforms/simulation/radio.c",
-        "examples/platforms/simulation/spi-stubs.c",
-        "examples/platforms/simulation/system.c",
-        "examples/platforms/simulation/trel.c",
-        "examples/platforms/simulation/uart.c",
-        "examples/platforms/utils/link_metrics.cpp",
-        "examples/platforms/utils/mac_frame.cpp",
-        "examples/platforms/utils/soft_source_match_table.c",
+        ":openthread_simulation_srcs",
         "src/lib/platform/exit_code.c",
         "third_party/mbedtls/repo/library/aes.c",
         "third_party/mbedtls/repo/library/asn1parse.c",
@@ -644,7 +716,7 @@
 
 cc_library_static {
     name: "openthread-radio",
-    vendor: true,
+    vendor_available: true,
 
     defaults: [
         "ot_rcp_cflags_defaults",
@@ -666,6 +738,7 @@
         "src/core/api/tasklet_api.cpp",
         "src/core/common/binary_search.cpp",
         "src/core/common/error.cpp",
+        "src/core/common/frame_builder.cpp",
         "src/core/common/instance.cpp",
         "src/core/common/log.cpp",
         "src/core/common/random.cpp",
@@ -688,12 +761,13 @@
         "src/core/radio/radio_platform.cpp",
         "src/core/thread/link_quality.cpp",
         "src/core/utils/parse_cmdline.cpp",
+        "src/core/utils/power_calibration.cpp",
     ],
 }
 
 cc_library_static {
     name: "openthread-hdlc",
-    vendor: true,
+    vendor_available: true,
     defaults: [
         "ot_rcp_cflags_defaults",
         "ot_simulation_cflags_defaults",
@@ -708,8 +782,35 @@
 }
 
 cc_library_static {
+    name: "openthread-spi",
+    vendor_available: true,
+    local_include_dirs: [
+        "include",
+        "src",
+        "src/core",
+        "src/lib/platform",
+        "src/posix/platform",
+        "src/posix/platform/include",
+    ],
+    export_include_dirs: [
+        "include",
+        "src/core",
+        "src/posix/platform",
+        "src/posix/platform/include",
+    ],
+
+    cflags: [
+        "-DOPENTHREAD_POSIX_CONFIG_RCP_BUS=OT_POSIX_RCP_BUS_SPI",
+    ],
+
+    srcs: [
+        "src/posix/platform/spi_interface.cpp",
+    ],
+}
+
+cc_library_static {
     name: "openthread-url",
-    vendor: true,
+    vendor_available: true,
     local_include_dirs: [
         "include",
         "src",
@@ -727,7 +828,7 @@
 
 cc_library_static {
     name: "openthread-platform",
-    vendor: true,
+    vendor_available: true,
     local_include_dirs: [
         "include",
         "src",
@@ -746,7 +847,7 @@
 
 cc_library_static {
     name: "openthread-spinel-rcp",
-    vendor: true,
+    vendor_available: true,
 
     defaults: [
         "ot_rcp_cflags_defaults",
@@ -768,7 +869,7 @@
 
 cc_library_static {
     name: "openthread-rcp",
-    vendor: true,
+    vendor_available: true,
 
     defaults: [
         "ot_rcp_cflags_defaults",
@@ -799,6 +900,9 @@
     ],
 
     static_libs: [
+        "libbase",
+        "libcutils",
+        "libutils",
         "openthread-hdlc",
         "openthread-radio",
         "openthread-spinel-rcp",
@@ -807,16 +911,12 @@
         "openthread-radio",
     ],
 
-    shared_libs: [
-        "libbase",
-        "libcutils",
-        "libutils",
-    ],
+    stl: "c++_static",
 }
 
 cc_library_static {
     name: "openthread-posix",
-    vendor: true,
+    vendor_available: true,
     local_include_dirs: [
         "include",
         "src",
@@ -850,7 +950,7 @@
 
 cc_library_static {
     name: "openthread-common",
-    vendor: true,
+    vendor_available: true,
     local_include_dirs: [
         "include",
         "src",
@@ -867,3 +967,35 @@
         "src/core/api/error_api.cpp",
     ],
 }
+
+cc_binary {
+    name: "ot-cli-ftd",
+    defaults: [
+        "ot_simulation_cflags_defaults",
+    ],
+
+    cflags: [
+        "-DOPENTHREAD_FTD=1",
+        "-DOPENTHREAD_CONFIG_POSIX_SETTINGS_PATH=\"/data/vendor/threadnetwork/simulation\"",
+    ],
+
+    srcs: [
+        ":openthread_core_srcs",
+        ":openthread_simulation_srcs",
+        ":openthread_cli_srcs",
+        ":openthread_mbedtls_srcs",
+        "examples/apps/cli/cli_uart.cpp",
+        "examples/apps/cli/main.c",
+    ],
+
+    static_libs: [
+        "openthread-platform",
+    ],
+
+    shared_libs: [
+        "libcutils", // Required by src/core/instance_api.cpp
+    ],
+
+    init_rc: ["src/android/ot-cli-ftd.rc"],
+    vendor: true,
+}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 642332f..15659e8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -33,6 +33,7 @@
 string(STRIP ${OT_DEFAULT_VERSION} OT_DEFAULT_VERSION)
 
 project(openthread VERSION ${OT_DEFAULT_VERSION})
+include(CTest)
 
 option(OT_BUILD_EXECUTABLES "Build executables" ON)
 option(OT_COVERAGE "enable coverage" OFF)
@@ -40,6 +41,9 @@
 option(OT_MBEDTLS_THREADING "enable mbedtls threading" OFF)
 
 add_library(ot-config INTERFACE)
+add_library(ot-config-ftd INTERFACE)
+add_library(ot-config-mtd INTERFACE)
+add_library(ot-config-radio INTERFACE)
 set(CMAKE_CXX_EXTENSIONS OFF)
 set(CMAKE_CXX_STANDARD 11)
 set(CMAKE_C_STANDARD 99)
@@ -103,13 +107,15 @@
 message(STATUS "Package Version: ${OT_PACKAGE_VERSION}")
 
 set(OT_THREAD_VERSION "1.3" CACHE STRING "Thread version chosen by the user at configure time")
-set_property(CACHE OT_THREAD_VERSION PROPERTY STRINGS "1.1" "1.2" "1.3")
+set_property(CACHE OT_THREAD_VERSION PROPERTY STRINGS "1.1" "1.2" "1.3" "1.3.1")
 if(${OT_THREAD_VERSION} EQUAL "1.1")
     target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_THREAD_VERSION=OT_THREAD_VERSION_1_1")
 elseif(${OT_THREAD_VERSION} EQUAL "1.2")
     target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_THREAD_VERSION=OT_THREAD_VERSION_1_2")
 elseif(${OT_THREAD_VERSION} EQUAL "1.3")
     target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_THREAD_VERSION=OT_THREAD_VERSION_1_3")
+elseif(${OT_THREAD_VERSION} EQUAL "1.3.1")
+    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_THREAD_VERSION=OT_THREAD_VERSION_1_3_1")
 else()
     message(FATAL_ERROR "Thread version unknown: ${OT_THREAD_VERSION}")
 endif()
@@ -169,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
@@ -201,11 +208,8 @@
 add_subdirectory(src)
 add_subdirectory(third_party EXCLUDE_FROM_ALL)
 
-if(OT_PLATFORM STREQUAL "simulation")
-    enable_testing()
-endif()
-
 add_subdirectory(tests)
+add_subdirectory(tools)
 
 add_custom_target(print-ot-config ALL
                   COMMAND ${CMAKE_COMMAND}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2e99d4d..ae564f2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -109,7 +109,7 @@
 
 #### Coding Conventions and Style
 
-OpenThread uses and enforces the [OpenThread Coding Conventions and Style](STYLE_GUIDE.md) on all code, except for code located in [third_party](third_party). Use `script/make-pretty` and `script/make-pretty check` to automatically reformat code and check for code-style compliance, respectively. OpenThread currently requires [clang-format v9.0.0](https://releases.llvm.org/download.html#9.0.0) for C/C++ and [yapf v0.31.0](https://github.com/google/yapf) for Python.
+OpenThread uses and enforces the [OpenThread Coding Conventions and Style](STYLE_GUIDE.md) on all code, except for code located in [third_party](third_party). Use `script/make-pretty` and `script/make-pretty check` to automatically reformat code and check for code-style compliance, respectively. OpenThread currently requires [clang-format v14.0.0](https://releases.llvm.org/download.html#14.0.0) for C/C++ and [yapf v0.31.0](https://github.com/google/yapf) for Python.
 
 As part of the cleanup process, you should also run `script/make-pretty check` to ensure that your code passes the baseline code style checks.
 
diff --git a/NOTICE b/NOTICE
index 94a2d71..04cdb0a 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,5 +1,5 @@
-OpenThread is an open source implementation of the Thread 1.2.0 Final Specification.
-The Thread 1.2.0 Final Specification is promulgated by the Thread Group. The Thread
+OpenThread is an open source implementation of the Thread 1.3.0 Final Specification.
+The Thread 1.3.0 Final Specification is promulgated by the Thread Group. The Thread
 Group is a non-profit organization formed for the purposes of defining one or
 more specifications, best practices, reference architectures, implementation
 guidelines and certification programs to promote the availability of compliant
@@ -7,10 +7,10 @@
 information about the benefits thereof, can be found at http://threadgroup.org.
 
 OpenThread is not affiliated with or endorsed by the Thread Group. Implementation
-of this OpenThread code does not assure compliance with the Thread 1.2.0 Final
+of this OpenThread code does not assure compliance with the Thread 1.3.0 Final
 Specification and does not convey the right to identify any final product as Thread
 certified. Members of the Thread Group may hold patents and other intellectual
-property rights relating to the Thread 1.2.0 Final Specification, ownership and
+property rights relating to the Thread 1.3.0 Final Specification, ownership and
 licenses of which are subject to the Thread Group’s IP Policies, and not this license.
 
 The included copyright to the OpenThread code is subject to the license in the
diff --git a/OWNERS b/OWNERS
index 888425b..55c307b 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,5 +1,3 @@
 # Bug component: 1203089
 
 include platform/packages/modules/ThreadNetwork:/OWNERS
-
-rquattle@google.com
diff --git a/README.md b/README.md
index 6574588..0c4a4f5 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![OpenThread][ot-logo]][ot-repo] [![Build][ot-gh-action-build-svg]][ot-gh-action-build] [![Simulation][ot-gh-action-simulation-svg]][ot-gh-action-simulation] [![Docker][ot-gh-action-docker-svg]][ot-gh-action-docker] [![Language grade: C/C++][ot-lgtm-svg]][ot-lgtm] [![Coverage Status][ot-codecov-svg]][ot-codecov]
+[![OpenThread][ot-logo]][ot-repo] [![Build][ot-gh-action-build-svg]][ot-gh-action-build] [![Simulation][ot-gh-action-simulation-svg]][ot-gh-action-simulation] [![Docker][ot-gh-action-docker-svg]][ot-gh-action-docker] [![Coverage Status][ot-codecov-svg]][ot-codecov]
 
 ---
 
@@ -10,7 +10,7 @@
 
 **...OS and platform agnostic**, with a narrow platform abstraction layer and a small memory footprint, making it highly portable. It supports both system-on-chip (SoC) and network co-processor (NCP) designs.
 
-**...a Thread Certified Component**, implementing all features defined in the [Thread 1.2 specification](https://www.threadgroup.org/support#specifications), including all Thread networking layers (IPv6, 6LoWPAN, IEEE 802.15.4 with MAC security, Mesh Link Establishment, Mesh Routing) and device roles, as well as [Border Router](https://github.com/openthread/ot-br-posix) support.
+**...a Thread Certified Component**, implementing all features defined in the [Thread 1.3.0 specification](https://www.threadgroup.org/support#specifications), including all Thread networking layers (IPv6, 6LoWPAN, IEEE 802.15.4 with MAC security, Mesh Link Establishment, Mesh Routing) and device roles, as well as [Border Router](https://github.com/openthread/ot-br-posix) support.
 
 More information about Thread can be found at [threadgroup.org](http://threadgroup.org/). Thread is a registered trademark of the Thread Group, Inc.
 
@@ -22,14 +22,12 @@
 [ot-gh-action-simulation-svg]: https://github.com/openthread/openthread/workflows/Simulation/badge.svg?branch=main&event=push
 [ot-gh-action-docker]: https://github.com/openthread/openthread/actions?query=workflow%3ADocker+branch%3Amain+event%3Apush
 [ot-gh-action-docker-svg]: https://github.com/openthread/openthread/workflows/Docker/badge.svg?branch=main&event=push
-[ot-lgtm]: https://lgtm.com/projects/g/openthread/openthread/context:cpp
-[ot-lgtm-svg]: https://img.shields.io/lgtm/grade/cpp/g/openthread/openthread.svg?logo=lgtm&logoWidth=18
 [ot-codecov]: https://codecov.io/gh/openthread/openthread
 [ot-codecov-svg]: https://codecov.io/gh/openthread/openthread/branch/main/graph/badge.svg
 
 # Who supports OpenThread?
 
-<a href="https://www.arm.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-arm.png" alt="ARM" width="200px"></a><a href="https://www.cascoda.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-cascoda.png" alt="Cascoda" width="200px"></a><a href="https://www.espressif.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-espressif-github.png" alt="Espressif" width="200px"></a><a href="https://www.google.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-google.png" alt="Google" width="200px"></a><a href="https://www.infineon.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-infineon.png" alt="Infineon" width="200px"></a><a href="http://www.nordicsemi.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-nordic.png" alt="Nordic" width="200px"></a><a href="http://www.nxp.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-nxp.png" alt="NXP" width="200px"></a><a href="http://www.qorvo.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-qorvo.png" alt="Qorvo" width="200px"></a><a href="https://www.qualcomm.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-qc.png" alt="Qualcomm" width="200px"></a><a href="https://www.samsung.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-samsung.png" alt="Samsung" width="200px"></a><a href="https://www.silabs.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-silabs.png" alt="Silicon Labs" width="200px"></a><a href="https://www.st.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-stm.png" alt="STMicroelectronics" width="200px"></a><a href="https://www.synopsys.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-synopsys.png" alt="Synopsys" width="200px"></a><a href="https://www.telink-semi.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-telink-github.png" alt="Telink Semiconductor" width="200px"></a><a href="https://www.ti.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-ti.png" alt="Texas Instruments" width="200px"></a><a href="https://www.zephyrproject.org/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-zephyr.png" alt="Zephyr Project" width="200px"></a>
+<a href="https://www.amazon.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-amazon.png" alt="Amazon" width="200px"></a><a href="https://www.arm.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-arm.png" alt="ARM" width="200px"></a><a href="https://www.cascoda.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-cascoda.png" alt="Cascoda" width="200px"></a><a href="https://www.eero.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-eero.png" alt="Eero" width="200px"></a><a href="https://www.espressif.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-espressif-github.png" alt="Espressif" width="200px"></a><a href="https://www.google.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-google.png" alt="Google" width="200px"></a><a href="https://www.infineon.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-infineon.png" alt="Infineon" width="200px"></a><a href="https://mmbnetworks.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-mmb-networks.png" alt="MMB Networks" width="200px"></a><a href="https://www.nabucasa.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-nabu-casa.png" alt="Nabu Casa" width="200px"></a><a href="https://www.nanoleaf.me/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-nanoleaf.png" alt="Nanoleaf" width="200px"></a><a href="http://www.nordicsemi.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-nordic.png" alt="Nordic" width="200px"></a><a href="http://www.nxp.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-nxp.png" alt="NXP" width="200px"></a><a href="http://www.qorvo.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-qorvo.png" alt="Qorvo" width="200px"></a><a href="https://www.qualcomm.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-qc.png" alt="Qualcomm" width="200px"></a><a href="https://www.samsung.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-samsung.png" alt="Samsung" width="200px"></a><a href="https://www.silabs.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-silabs.png" alt="Silicon Labs" width="200px"></a><a href="https://www.st.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-stm.png" alt="STMicroelectronics" width="200px"></a><a href="https://www.synopsys.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-synopsys.png" alt="Synopsys" width="200px"></a><a href="https://www.telink-semi.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-telink-github.png" alt="Telink Semiconductor" width="200px"></a><a href="https://www.ti.com/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-ti.png" alt="Texas Instruments" width="200px"></a><a href="https://www.zephyrproject.org/"><img src="https://github.com/openthread/openthread/raw/main/doc/images/ot-contrib-zephyr.png" alt="Zephyr Project" width="200px"></a>
 
 # Getting started
 
@@ -54,10 +52,6 @@
 
 Contributors are required to abide by our [Code of Conduct](https://github.com/openthread/openthread/blob/main/CODE_OF_CONDUCT.md) and [Coding Conventions and Style Guide](https://github.com/openthread/openthread/blob/main/STYLE_GUIDE.md).
 
-# Versioning
-
-OpenThread follows the [Semantic Versioning guidelines](http://semver.org/) for release cycle transparency and to maintain backwards compatibility. OpenThread's versioning is independent of the Thread protocol specification version but will clearly indicate which version of the specification it currently supports.
-
 # License
 
 OpenThread is released under the [BSD 3-Clause license](https://github.com/openthread/openthread/blob/main/LICENSE). See the [`LICENSE`](https://github.com/openthread/openthread/blob/main/LICENSE) file for more information.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..455cf9a
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1 @@
+To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). We use g.co/vulnz for our intake, and do coordination and disclosure here on GitHub (including using GitHub Security Advisory). The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md
index d8ea624..c409c32 100644
--- a/STYLE_GUIDE.md
+++ b/STYLE_GUIDE.md
@@ -116,7 +116,7 @@
 
 - OpenThread uses `script/make-pretty` to reformat code and enforce code format and style. `script/make-pretty check` build target is included in OpenThread's continuous integration and must pass before a pull request is merged.
 
-- `script/make-pretty` requires [clang-format v9.0.0](https://releases.llvm.org/download.html#9.0.0) for C/C++ and [yapf v0.31.0](https://github.com/google/yapf) for Python.
+- `script/make-pretty` requires [clang-format v14.0.0](https://releases.llvm.org/download.html#14.0.0) for C/C++ and [yapf v0.31.0](https://github.com/google/yapf) for Python.
 
 ### File Names
 
diff --git a/configure.ac b/configure.ac
index 8c0c7fc..dedd85d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -804,14 +804,14 @@
 
 AC_ARG_WITH(examples,
     [AS_HELP_STRING([--with-examples=TARGET],
-        [Build example applications for one of: simulation, cc2538 @<:@default=no@:>@.
+        [Build example applications for one of: simulation @<:@default=no@:>@.
          Note that building example applications also builds the associated OpenThread platform libraries
          and any third_party libraries needed to support the examples.])],
     [
         case "${with_examples}" in
         no)
             ;;
-        simulation|cc2538)
+        simulation)
             ;;
         *)
             AC_MSG_RESULT(ERROR)
@@ -824,7 +824,6 @@
 AM_CONDITIONAL([OPENTHREAD_ENABLE_EXAMPLES], [test ${with_examples} != "no"])
 
 AM_CONDITIONAL([OPENTHREAD_EXAMPLES_SIMULATION],[test "${with_examples}" = "simulation"])
-AM_CONDITIONAL([OPENTHREAD_EXAMPLES_CC2538],    [test "${with_examples}" = "cc2538"])
 
 AM_COND_IF([OPENTHREAD_EXAMPLES_SIMULATION], CPPFLAGS="${CPPFLAGS} -DOPENTHREAD_EXAMPLES_SIMULATION=1", CPPFLAGS="${CPPFLAGS} -DOPENTHREAD_EXAMPLES_SIMULATION=0")
 
@@ -845,11 +844,11 @@
 
 AC_ARG_WITH(platform,
     [AS_HELP_STRING([--with-platform=TARGET],
-        [Build OpenThread platform libraries for one of: cc2538, posix, simulation @<:@default=simulation@:>@.])],
+        [Build OpenThread platform libraries for one of: posix, simulation @<:@default=simulation@:>@.])],
     [
         # Make sure the given target is valid.
         case "${with_platform}" in
-        no|cc2538|posix|simulation)
+        no|posix|simulation)
             ;;
         *)
             AC_MSG_RESULT(ERROR)
@@ -880,7 +879,6 @@
 
 OPENTHREAD_ENABLE_PLATFORM=${with_platform}
 
-AM_CONDITIONAL([OPENTHREAD_PLATFORM_CC2538],    [test "${with_platform}" = "cc2538"])
 AM_CONDITIONAL([OPENTHREAD_PLATFORM_POSIX],     [test "${with_platform}" = "posix"])
 AM_CONDITIONAL([OPENTHREAD_PLATFORM_SIMULATION],[test "${with_platform}" = "simulation"])
 
@@ -1023,7 +1021,6 @@
 examples/apps/cli/Makefile
 examples/apps/ncp/Makefile
 examples/platforms/Makefile
-examples/platforms/cc2538/Makefile
 examples/platforms/simulation/Makefile
 examples/platforms/utils/Makefile
 tools/Makefile
@@ -1032,9 +1029,6 @@
 tools/spi-hdlc-adapter/Makefile
 tests/Makefile
 tests/fuzz/Makefile
-tests/scripts/Makefile
-tests/scripts/thread-cert/Makefile
-tests/unit/Makefile
 doc/Makefile
 ])
 
diff --git a/doc/images/ot-contrib-amazon.png b/doc/images/ot-contrib-amazon.png
new file mode 100644
index 0000000..ac236ee
--- /dev/null
+++ b/doc/images/ot-contrib-amazon.png
Binary files differ
diff --git a/doc/images/ot-contrib-eero.png b/doc/images/ot-contrib-eero.png
new file mode 100644
index 0000000..317f20c
--- /dev/null
+++ b/doc/images/ot-contrib-eero.png
Binary files differ
diff --git a/doc/images/ot-contrib-mmb-networks.png b/doc/images/ot-contrib-mmb-networks.png
new file mode 100644
index 0000000..e0f7dd5
--- /dev/null
+++ b/doc/images/ot-contrib-mmb-networks.png
Binary files differ
diff --git a/doc/images/ot-contrib-nabu-casa.png b/doc/images/ot-contrib-nabu-casa.png
new file mode 100644
index 0000000..f3b637b
--- /dev/null
+++ b/doc/images/ot-contrib-nabu-casa.png
Binary files differ
diff --git a/doc/images/ot-contrib-nanoleaf.png b/doc/images/ot-contrib-nanoleaf.png
new file mode 100644
index 0000000..ca18d20
--- /dev/null
+++ b/doc/images/ot-contrib-nanoleaf.png
Binary files differ
diff --git a/doc/images/ot-contrib-qorvo.png b/doc/images/ot-contrib-qorvo.png
index f6815fc..5a9fc48 100644
--- a/doc/images/ot-contrib-qorvo.png
+++ b/doc/images/ot-contrib-qorvo.png
Binary files differ
diff --git a/doc/ot_api_doc.h b/doc/ot_api_doc.h
index 3d58b68..58e09a9 100644
--- a/doc/ot_api_doc.h
+++ b/doc/ot_api_doc.h
@@ -53,7 +53,7 @@
  * @defgroup api-net                  IPv6 Networking
  * @{
  *
- * @defgroup api-dns                  DNSv6
+ * @defgroup api-dns                  DNS
  * @defgroup api-dnssd-server         DNS-SD Server
  * @defgroup api-icmp6                ICMPv6
  * @defgroup api-ip6                  IPv6
@@ -109,6 +109,7 @@
  * @brief This module includes functions for all Thread roles.
  * @defgroup api-joiner               Joiner
  * @defgroup api-operational-dataset  Operational Dataset
+ * @brief Includes functions for the Operational Dataset API.
  * @defgroup api-thread-router        Router/Leader
  * @brief This module includes functions for Thread Routers and Leaders.
  * @defgroup api-server               Server
@@ -138,6 +139,7 @@
  * @defgroup api-history-tracker      History Tracker
  * @defgroup api-jam-detection        Jam Detection
  * @defgroup api-logging              Logging - Thread Stack
+ * @defgroup api-mesh-diag            Mesh Diagnostics
  * @defgroup api-ncp                  Network Co-Processor
  * @defgroup api-network-time         Network Time Synchronization
  * @defgroup api-random-group         Random Number Generator
@@ -166,6 +168,7 @@
  *
  * @defgroup plat-alarm               Alarm
  * @defgroup plat-crypto              Crypto - Platform
+ * @defgroup plat-dns                 DNS - Platform
  * @defgroup plat-entropy             Entropy
  * @defgroup plat-factory-diagnostics Factory Diagnostics - Platform
  * @defgroup plat-logging             Logging - Platform
diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake
index 521b7fa..6afef68 100644
--- a/etc/cmake/options.cmake
+++ b/etc/cmake/options.cmake
@@ -34,57 +34,194 @@
 option(OT_MTD "enable MTD" ON)
 option(OT_RCP "enable RCP" ON)
 
-option(OT_ANYCAST_LOCATOR "enable anycast locator support")
-if(OT_ANYCAST_LOCATOR)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE=1")
+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})
+message(STATUS OT_FTD=${OT_FTD})
+message(STATUS OT_MTD=${OT_MTD})
+message(STATUS OT_RCP=${OT_RCP})
+
+set(OT_CONFIG_VALUES
+    ""
+    "ON"
+    "OFF"
+)
+
+macro(ot_option name ot_config description)
+    # Declare an OT cmake config with `name` mapping to OPENTHREAD_CONFIG
+    # `ot_config`. Parameter `description` provides the help string for this
+    # OT cmake config. There is an optional last parameter which if provided
+    # determines the default value for the cmake config. If not provided
+    # empty string is used which will be treated as "not specified". In this
+    # case, the variable `name` would still be false but the related
+    # OPENTHREAD_CONFIG is not added in `ot-config`.
+
+    if (${ARGC} GREATER 3)
+        set(${name} ${ARGN} CACHE STRING "enable ${description}")
+    else()
+        set(${name} "" CACHE STRING "enable ${description}")
+    endif()
+
+    set_property(CACHE ${name} PROPERTY STRINGS ${OT_CONFIG_VALUES})
+
+    string(COMPARE EQUAL "${${name}}" "" is_empty)
+    if (is_empty)
+        message(STATUS "${name}=\"\"")
+    elseif (${name})
+        message(STATUS "${name}=ON --> ${ot_config}=1")
+        target_compile_definitions(ot-config INTERFACE "${ot_config}=1")
+    else()
+        message(STATUS "${name}=OFF --> ${ot_config}=0")
+        target_compile_definitions(ot-config INTERFACE "${ot_config}=0")
+    endif()
+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")
+ot_option(OT_CHANNEL_MANAGER OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE "channel manager")
+ot_option(OT_CHANNEL_MONITOR OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE "channel monitor")
+ot_option(OT_COAP OPENTHREAD_CONFIG_COAP_API_ENABLE "coap api")
+ot_option(OT_COAP_BLOCK OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE "coap block-wise transfer (RFC7959)")
+ot_option(OT_COAP_OBSERVE OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE "coap observe (RFC7641)")
+ot_option(OT_COAPS OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE "secure coap")
+ot_option(OT_COMMISSIONER OPENTHREAD_CONFIG_COMMISSIONER_ENABLE "commissioner")
+ot_option(OT_CSL_AUTO_SYNC OPENTHREAD_CONFIG_MAC_CSL_AUTO_SYNC_ENABLE "data polling based on csl")
+ot_option(OT_CSL_DEBUG OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE "csl debug")
+ot_option(OT_CSL_RECEIVER OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE "csl receiver")
+ot_option(OT_DATASET_UPDATER OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE "dataset updater")
+ot_option(OT_DHCP6_CLIENT OPENTHREAD_CONFIG_DHCP6_CLIENT_ENABLE "DHCP6 client")
+ot_option(OT_DHCP6_SERVER OPENTHREAD_CONFIG_DHCP6_SERVER_ENABLE "DHCP6 server")
+ot_option(OT_DIAGNOSTIC OPENTHREAD_CONFIG_DIAG_ENABLE "diagnostic")
+ot_option(OT_DNS_CLIENT OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE "DNS client")
+ot_option(OT_DNS_CLIENT_OVER_TCP OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE  "Enable dns query over tcp")
+ot_option(OT_DNS_DSO OPENTHREAD_CONFIG_DNS_DSO_ENABLE "DNS Stateful Operations (DSO)")
+ot_option(OT_DNS_UPSTREAM_QUERY OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE "Allow sending DNS queries to upstream")
+ot_option(OT_DNSSD_SERVER OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE "DNS-SD server")
+ot_option(OT_DUA OPENTHREAD_CONFIG_DUA_ENABLE "Domain Unicast Address (DUA)")
+ot_option(OT_ECDSA OPENTHREAD_CONFIG_ECDSA_ENABLE "ECDSA")
+ot_option(OT_EXTERNAL_HEAP OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE "external heap")
+ot_option(OT_FIREWALL OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE "firewall")
+ot_option(OT_HISTORY_TRACKER OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE "history tracker")
+ot_option(OT_IP6_FRAGM OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE "ipv6 fragmentation")
+ot_option(OT_JAM_DETECTION OPENTHREAD_CONFIG_JAM_DETECTION_ENABLE "jam detection")
+ot_option(OT_JOINER OPENTHREAD_CONFIG_JOINER_ENABLE "joiner")
+ot_option(OT_LINK_METRICS_INITIATOR OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE "link metrics initiator")
+ot_option(OT_LINK_METRICS_SUBJECT OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE "link metrics subject")
+ot_option(OT_LINK_RAW OPENTHREAD_CONFIG_LINK_RAW_ENABLE "link raw service")
+ot_option(OT_LOG_LEVEL_DYNAMIC OPENTHREAD_CONFIG_LOG_LEVEL_DYNAMIC_ENABLE "dynamic log level control")
+ot_option(OT_MAC_FILTER OPENTHREAD_CONFIG_MAC_FILTER_ENABLE "mac filter")
+ot_option(OT_MESH_DIAG OPENTHREAD_CONFIG_MESH_DIAG_ENABLE "mesh diag")
+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_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")
+ot_option(OT_PLATFORM_UDP OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE "platform UDP")
+ot_option(OT_REFERENCE_DEVICE OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE "test harness reference device")
+ot_option(OT_SERVICE OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE "Network Data service")
+ot_option(OT_SETTINGS_RAM OPENTHREAD_SETTINGS_RAM "volatile-only storage of settings")
+ot_option(OT_SLAAC OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE "SLAAC address")
+ot_option(OT_SNTP_CLIENT OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE "SNTP client")
+ot_option(OT_SRP_CLIENT OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE "SRP client")
+ot_option(OT_SRP_SERVER OPENTHREAD_CONFIG_SRP_SERVER_ENABLE "SRP server")
+ot_option(OT_TCP OPENTHREAD_CONFIG_TCP_ENABLE "TCP")
+ot_option(OT_TIME_SYNC OPENTHREAD_CONFIG_TIME_SYNC_ENABLE "time synchronization service")
+ot_option(OT_TREL OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE "TREL radio link for Thread over Infrastructure feature")
+ot_option(OT_TX_BEACON_PAYLOAD OPENTHREAD_CONFIG_MAC_OUTGOING_BEACON_PAYLOAD_ENABLE "tx beacon payload")
+ot_option(OT_UDP_FORWARD OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE "UDP forward")
+ot_option(OT_UPTIME OPENTHREAD_CONFIG_UPTIME_ENABLE "uptime")
+
+option(OT_DOC "Build OpenThread documentation")
+
+option(OT_FULL_LOGS "enable full logs")
+if(OT_FULL_LOGS)
+    if(NOT OT_LOG_LEVEL)
+        message(STATUS "OT_FULL_LOGS=ON --> Setting LOG_LEVEL to DEBG")
+        target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_LOG_LEVEL=OT_LOG_LEVEL_DEBG")
+    endif()
+    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_LOG_PREPEND_LEVEL=1")
 endif()
 
-option(OT_ASSERT "enable assert function OT_ASSERT()" ON)
-if(OT_ASSERT)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_ASSERT_ENABLE=1")
+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()
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_ASSERT_ENABLE=0")
+    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()
 
-option(OT_BACKBONE_ROUTER "enable backbone router functionality")
-if(OT_BACKBONE_ROUTER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE=1")
-    set(OT_BACKBONE_ROUTER_DUA_NDPROXYING ON CACHE BOOL "Enable DUA NDProxying by default")
-    set(OT_BACKBONE_ROUTER_MULTICAST_ROUTING ON CACHE BOOL "Enable Multicast Routing by default")
-endif()
-
-option(OT_BACKBONE_ROUTER_DUA_NDPROXYING "enable Backbone Router DUA ND Proxying functionality" OFF)
-if(OT_BACKBONE_ROUTER_DUA_NDPROXYING)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE=1")
+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()
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE=0")
+    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()
 
-option(OT_BACKBONE_ROUTER_MULTICAST_ROUTING "enable Backbone Router Multicast Routing functionality" OFF)
-if(OT_BACKBONE_ROUTER_MULTICAST_ROUTING)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE=1")
+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()
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE=0")
+    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()
 
-option(OT_BORDER_AGENT "enable border agent support")
-if(OT_BORDER_AGENT)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE=1")
+set(OT_POWER_SUPPLY "" CACHE STRING "set the device power supply config")
+set(OT_POWER_SUPPLY_VALUES
+    ""
+    "BATTERY"
+    "EXTERNAL"
+    "EXTERNAL_STABLE"
+    "EXTERNAL_UNSTABLE"
+)
+set_property(CACHE OT_POWER_SUPPLY PROPERTY STRINGS ${OT_POWER_SUPPLY_VALUES})
+string(COMPARE EQUAL "${OT_POWER_SUPPLY}" "" is_empty)
+if (is_empty)
+    message(STATUS "OT_POWER_SUPPLY=\"\"")
+else()
+    message(STATUS "OT_POWER_SUPPLY=${OT_POWER_SUPPLY} --> OPENTHREAD_CONFIG_DEVICE_POWER_SUPPLY=OT_POWER_SUPPLY_${OT_POWER_SUPPLY}")
+    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DEVICE_POWER_SUPPLY=OT_POWER_SUPPLY_${OT_POWER_SUPPLY}")
 endif()
 
-option(OT_BORDER_ROUTER "enable border router support")
-if(OT_BORDER_ROUTER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE=1")
+set(OT_MLE_MAX_CHILDREN "" CACHE STRING "set maximum number of children")
+if(OT_MLE_MAX_CHILDREN MATCHES "^[0-9]+$")
+    message(STATUS "OT_MLE_MAX_CHILDREN=${OT_MLE_MAX_CHILDREN}")
+    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MLE_MAX_CHILDREN=${OT_MLE_MAX_CHILDREN}")
+elseif(NOT OT_MLE_MAX_CHILDREN STREQUAL "")
+    message(FATAL_ERROR "Invalid maximum number of children: ${OT_MLE_MAX_CHILDREN}")
 endif()
 
-option(OT_BORDER_ROUTING "enable border routing support")
-if(OT_BORDER_ROUTING)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE=1")
-endif()
-
-option(OT_BORDER_ROUTING_NAT64 "enable border routing NAT64 support")
-if(OT_BORDER_ROUTING_NAT64)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE=1")
+set(OT_RCP_RESTORATION_MAX_COUNT "0" CACHE STRING "set max RCP restoration count")
+if(OT_RCP_RESTORATION_MAX_COUNT MATCHES "^[0-9]+$")
+    message(STATUS "OT_RCP_RESTORATION_MAX_COUNT=${OT_RCP_RESTORATION_MAX_COUNT}")
+    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT=${OT_RCP_RESTORATION_MAX_COUNT}")
+else()
+    message(FATAL_ERROR "Invalid max RCP restoration count: ${OT_RCP_RESTORATION_MAX_COUNT}")
 endif()
 
 if(NOT OT_EXTERNAL_MBEDTLS)
@@ -102,307 +239,21 @@
     target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_ENABLE_BUILTIN_MBEDTLS_MANAGEMENT=0")
 endif()
 
-option(OT_CHANNEL_MANAGER "enable channel manager support")
-if(OT_CHANNEL_MANAGER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE=1")
-endif()
-
-option(OT_CHANNEL_MONITOR "enable channel monitor support")
-if(OT_CHANNEL_MONITOR)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE=1")
-endif()
-
-option(OT_CHILD_SUPERVISION "enable child supervision support")
-if(OT_CHILD_SUPERVISION)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE=1")
-endif()
-
-option(OT_COAP "enable coap api support")
-if(OT_COAP)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_COAP_API_ENABLE=1")
-endif()
-
-option(OT_COAPS "enable secure coap api support")
-if(OT_COAPS)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE=1")
-endif()
-
-option(OT_COAP_BLOCK "enable coap block-wise transfer (RFC7959) api support")
-if(OT_COAP_BLOCK)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE=1")
-endif()
-
-option(OT_COAP_OBSERVE "enable coap observe (RFC7641) api support")
-if(OT_COAP_OBSERVE)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE=1")
-endif()
-
-option(OT_COMMISSIONER "enable commissioner support")
-if(OT_COMMISSIONER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_COMMISSIONER_ENABLE=1")
-endif()
-
-option(OT_CSL_RECEIVER "enable csl receiver")
-if(OT_CSL_RECEIVER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE=1")
-endif()
-
-option(OT_CSL_AUTO_SYNC "enable data polling based on csl config" ${OT_CSL_RECEIVER})
-if(OT_CSL_AUTO_SYNC)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MAC_CSL_AUTO_SYNC_ENABLE=1")
-else()
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MAC_CSL_AUTO_SYNC_ENABLE=0")
-endif()
-
-option(OT_CSL_DEBUG "enable csl debug")
-if(OT_CSL_DEBUG)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE=1")
-endif()
-
-option(OT_DATASET_UPDATER "enable dataset updater support")
-if(OT_DATASET_UPDATER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE=1")
-endif()
-
-option(OT_DHCP6_CLIENT "enable DHCP6 client support")
-if(OT_DHCP6_CLIENT)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DHCP6_CLIENT_ENABLE=1")
-endif()
-
-option(OT_DHCP6_SERVER "enable DHCP6 server support")
-if(OT_DHCP6_SERVER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DHCP6_SERVER_ENABLE=1")
-endif()
-
-option(OT_DIAGNOSTIC "enable diagnostic support")
-if(OT_DIAGNOSTIC)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DIAG_ENABLE=1")
-endif()
-
-option(OT_DNS_CLIENT "enable DNS client support")
-if(OT_DNS_CLIENT)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE=1")
-endif()
-
-option(OT_DNS_DSO "enable DNS Stateful Operations (DSO) support")
-if(OT_DNS_DSO)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DNS_DSO_ENABLE=1")
-endif()
-
-option(OT_DNSSD_SERVER "enable DNS-SD server support")
-if(OT_DNSSD_SERVER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE=1")
-endif()
-
-option(OT_DOC "Build OpenThread documentation")
-
-option(OT_ECDSA "enable ECDSA support")
-if(OT_ECDSA)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_ECDSA_ENABLE=1")
-endif()
-
-option(OT_SRP_CLIENT "enable SRP client support")
-if (OT_SRP_CLIENT)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE=1")
-endif()
-
-option(OT_DUA "enable Domain Unicast Address feature for Thread 1.2")
-if(OT_DUA)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_DUA_ENABLE=1")
-endif()
-
-option(OT_MESSAGE_USE_HEAP "enable heap allocator for message buffers")
-if(OT_MESSAGE_USE_HEAP)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MESSAGE_USE_HEAP_ENABLE=1")
-endif()
-
-option(OT_MLR "enable Multicast Listener Registration feature for Thread 1.2")
-if(OT_MLR)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MLR_ENABLE=1")
-endif()
-
-option(OT_EXTERNAL_HEAP "enable external heap support")
-if(OT_EXTERNAL_HEAP)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE=1")
-endif()
-
-option(OT_HISTORY_TRACKER "enable history tracker support")
-if(OT_HISTORY_TRACKER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE=1")
-endif()
-
-option(OT_IP6_FRAGM "enable ipv6 fragmentation support")
-if(OT_IP6_FRAGM)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE=1")
-endif()
-
-option(OT_JAM_DETECTION "enable jam detection support")
-if(OT_JAM_DETECTION)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_JAM_DETECTION_ENABLE=1")
-endif()
-
-option(OT_JOINER "enable joiner support")
-if(OT_JOINER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_JOINER_ENABLE=1")
-endif()
-
-option(OT_LEGACY "enable legacy network support")
-if(OT_LEGACY)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_LEGACY_ENABLE=1")
-endif()
-
-option(OT_LINK_RAW "enable link raw service")
-if(OT_LINK_RAW)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_LINK_RAW_ENABLE=1")
-endif()
-
-option(OT_LINK_METRICS_INITIATOR "enable link metrics initiator")
-if (OT_LINK_METRICS_INITIATOR)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE=1")
-endif()
-
-option(OT_LINK_METRICS_SUBJECT "enable link metrics subject")
-if (OT_LINK_METRICS_SUBJECT)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE=1")
-endif()
-
-option(OT_LOG_LEVEL_DYNAMIC "enable dynamic log level control")
-if(OT_LOG_LEVEL_DYNAMIC)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_LOG_LEVEL_DYNAMIC_ENABLE=1")
-endif()
-
-option(OT_MAC_FILTER "enable mac filter support")
-if(OT_MAC_FILTER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MAC_FILTER_ENABLE=1")
-endif()
-
-option(OT_MLE_LONG_ROUTES "enable MLE long routes extension (experimental, breaks Thread conformance)")
-if(OT_MLE_LONG_ROUTES)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MLE_LONG_ROUTES_ENABLE=1")
-endif()
-
-option(OT_MTD_NETDIAG "enable TMF network diagnostics on MTDs")
-if(OT_MTD_NETDIAG)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE=1")
-endif()
-
-option(OT_MULTIPLE_INSTANCE "enable multiple instances")
-if(OT_MULTIPLE_INSTANCE)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE=1")
-endif()
-
-option(OT_NEIGHBOR_DISCOVERY_AGENT "enable neighbor discovery agent support")
-if(OT_NEIGHBOR_DISCOVERY_AGENT)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_NEIGHBOR_DISCOVERY_AGENT_ENABLE=1")
-endif()
-
-option(OT_NETDATA_PUBLISHER "enable Thread Network Data publisher")
-if(OT_NETDATA_PUBLISHER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE=1")
-endif()
-
-option(OT_PING_SENDER "enable ping sender support" ${OT_APP_CLI})
-if(OT_PING_SENDER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_PING_SENDER_ENABLE=1")
-endif()
-
-option(OT_PLATFORM_NETIF "enable platform netif support")
-if(OT_PLATFORM_NETIF)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE=1")
-endif()
-
-option(OT_PLATFORM_UDP "enable platform UDP support")
-if(OT_PLATFORM_UDP)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE=1")
-endif()
-
 if(OT_POSIX_SETTINGS_PATH)
     target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_POSIX_SETTINGS_PATH=${OT_POSIX_SETTINGS_PATH}")
 endif()
 
-option(OT_REFERENCE_DEVICE "enable Thread Test Harness reference device support")
-if(OT_REFERENCE_DEVICE)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE=1")
-endif()
+#-----------------------------------------------------------------------------------------------------------------------
+# Check removed/replaced options
 
-option(OT_SERVICE "enable support for injecting Service entries into the Thread Network Data")
-if(OT_SERVICE)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE=1")
-endif()
-
-option(OT_SETTINGS_RAM "enable volatile-only storage of settings")
-if(OT_SETTINGS_RAM)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_SETTINGS_RAM=1")
-endif()
-
-option(OT_SLAAC "enable support for adding of auto-configured SLAAC addresses by OpenThread")
-if(OT_SLAAC)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE=1")
-endif()
-
-option(OT_SNTP_CLIENT "enable SNTP Client support")
-if(OT_SNTP_CLIENT)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE=1")
-endif()
-
-option(OT_SRP_SERVER "enable SRP server")
-if (OT_SRP_SERVER)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_SRP_SERVER_ENABLE=1")
-endif()
-
-option(OT_TIME_SYNC "enable the time synchronization service feature")
-if(OT_TIME_SYNC)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_TIME_SYNC_ENABLE=1")
-endif()
-
-option(OT_TREL "enable TREL radio link for Thread over Infrastructure feature")
-if (OT_TREL)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE=1")
-endif()
-
-option(OT_TX_BEACON_PAYLOAD "enable Thread beacon payload in outgoing beacons")
-if (OT_TX_BEACON_PAYLOAD)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_MAC_OUTGOING_BEACON_PAYLOAD_ENABLE=1")
-endif()
-
-option(OT_UDP_FORWARD "enable UDP forward support")
-if(OT_UDP_FORWARD)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE=1")
-endif()
-
-option(OT_UPTIME "enable support for tracking OpenThread instance's uptime")
-if(OT_UPTIME)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_UPTIME_ENABLE=1")
-endif()
-
-option(OT_FIREWALL "enable firewall")
-if (OT_FIREWALL)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE=1")
-endif()
-
-option(OT_FULL_LOGS "enable full logs")
-if(OT_FULL_LOGS)
-    if(NOT OT_LOG_LEVEL)
-        target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_LOG_LEVEL=OT_LOG_LEVEL_DEBG")
+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()
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_LOG_PREPEND_LEVEL=1")
-endif()
+endmacro()
 
-option(OT_OTNS "enable OTNS support")
-if(OT_OTNS)
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_OTNS_ENABLE=1")
-endif()
-
-set(OT_RCP_RESTORATION_MAX_COUNT "0" CACHE STRING "set max RCP restoration count")
-if(OT_RCP_RESTORATION_MAX_COUNT MATCHES "^[0-9]+$")
-    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT=${OT_RCP_RESTORATION_MAX_COUNT}")
-else()
-    message(FATAL_ERROR "Invalid max RCP restoration count: ${OT_RCP_RESTORATION_MAX_COUNT}")
-endif()
-
-option(OT_EXCLUDE_TCPLP_LIB "exclude TCPlp library from build")
-
-# Checks
-if(OT_PLATFORM_UDP AND OT_UDP_FORWARD)
-    message(FATAL_ERROR "OT_PLATFORM_UDP and OT_UDP_FORWARD are exclusive")
-endif()
+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/docker/environment/Dockerfile b/etc/docker/environment/Dockerfile
index 3c297de..a7c38f8 100644
--- a/etc/docker/environment/Dockerfile
+++ b/etc/docker/environment/Dockerfile
@@ -1,5 +1,5 @@
 # Ubuntu image with tools required to build OpenThread
-FROM ubuntu:18.04
+FROM ubuntu:22.04
 
 ENV DEBIAN_FRONTEND noninteractive
 ENV LANG en_US.UTF-8
@@ -9,6 +9,7 @@
     && apt-get install -y locales \
     && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \
     && apt-get --no-install-recommends install -fy \
+        bzip2 \
         git \
         ninja-build \
         python3 \
@@ -20,6 +21,7 @@
         inetutils-ping \
         ca-certificates \
     && update-ca-certificates \
+    && python3 -m pip install -U pip \
     && python3 -m pip install -U cmake \
     && python3 -m pip install wheel
 
diff --git a/etc/gn/openthread.gni b/etc/gn/openthread.gni
index fe494b0..9bbb543 100644
--- a/etc/gn/openthread.gni
+++ b/etc/gn/openthread.gni
@@ -84,6 +84,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
 
@@ -174,8 +177,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-cc2538 b/examples/Makefile-cc2538
deleted file mode 100644
index 99d895a..0000000
--- a/examples/Makefile-cc2538
+++ /dev/null
@@ -1,309 +0,0 @@
-#
-#  Copyright (c) 2016, 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.
-#
-
-.NOTPARALLEL:
-
-AR                              = arm-none-eabi-ar
-CCAS                            = arm-none-eabi-as
-CPP                             = arm-none-eabi-cpp
-CC                              = arm-none-eabi-gcc
-CXX                             = arm-none-eabi-g++
-LD                              = arm-none-eabi-ld
-STRIP                           = arm-none-eabi-strip
-NM                              = arm-none-eabi-nm
-RANLIB                          = arm-none-eabi-ranlib
-OBJCOPY                         = arm-none-eabi-objcopy
-
-BuildJobs                      ?= 10
-
-configure_OPTIONS               = \
-    --enable-cli                  \
-    --enable-ftd                  \
-    --enable-mtd                  \
-    --enable-ncp                  \
-    --enable-radio-only           \
-    --enable-linker-map           \
-    --with-examples=cc2538        \
-    $(NULL)
-
-TopSourceDir                    := $(dir $(shell readlink $(firstword $(MAKEFILE_LIST))))..
-AbsTopSourceDir                 := $(dir $(realpath $(firstword $(MAKEFILE_LIST))))..
-
-CC2538_CONFIG_FILE_CPPFLAGS  = -DOPENTHREAD_PROJECT_CORE_CONFIG_FILE='\"openthread-core-cc2538-config.h\"'
-CC2538_CONFIG_FILE_CPPFLAGS += -DOPENTHREAD_CORE_CONFIG_PLATFORM_CHECK_FILE='\"openthread-core-cc2538-config-check.h\"'
-CC2538_CONFIG_FILE_CPPFLAGS += -I$(AbsTopSourceDir)/examples/platforms/cc2538/
-
-COMMONCFLAGS                    := \
-    -fdata-sections                \
-    -ffunction-sections            \
-    -Os                            \
-    -g                             \
-    $(CC2538_CONFIG_FILE_CPPFLAGS) \
-    $(NULL)
-
-include $(dir $(abspath $(lastword $(MAKEFILE_LIST))))/common-switches.mk
-
-# Optional CC2592 options, first and foremost, whether to enable support for it
-# at all.
-ifeq ($(CC2592),1)
-COMMONCFLAGS += -DOPENTHREAD_CONFIG_CC2538_WITH_CC2592=1
-
-# If the PA_EN is on another port C pin, specify it with CC2592_PA_PIN.
-ifneq ($(CC2592_PA_EN),)
-COMMONCFLAGS += -DOPENTHREAD_CONFIG_CC2592_PA_EN_PIN=$(CC2592_PA_EN)
-endif
-
-# If the LNA_EN is on another port C pin, specify it with CC2592_LNA_PIN.
-ifneq ($(CC2592_LNA_EN),)
-COMMONCFLAGS += -DOPENTHREAD_CONFIG_CC2592_LNA_EN_PIN=$(CC2592_LNA_EN)
-endif
-
-# If we're not using HGM, set CC2538_USE_HGM to 0.
-ifeq ($(CC2592_USE_HGM),0)
-COMMONCFLAGS += -DOPENTHREAD_CONFIG_CC2592_USE_HGM=0
-else # CC2592_USE_HGM=1
-
-# HGM in use, if not on port D, specify the port here (A, B or C) with CC2592_HGM_PORT.
-ifneq ($(CC2592_HGM_PORT),)
-COMMONCFLAGS += -DOPENTHREAD_CONFIG_CC2592_HGM_PORT=GPIO_$(CC2592_HGM_PORT)_BASE
-endif
-
-# If HGM is not at pin 2, specify which pin here with CC2592_HGM_PIN.
-ifneq ($(CC2592_HGM_PIN),)
-COMMONCFLAGS += -DOPENTHREAD_CONFIG_CC2592_HGM_PIN=$(CC2592_HGM_PIN)
-endif
-
-# If we want it off by default, specify CC2592_HGM_DEFAULT_STATE=0
-ifeq ($(CC2592_HGM_DEFAULT_STATE),0)
-COMMONCFLAGS += -DOPENTHREAD_CONFIG_CC2592_HGM_DEFAULT_STATE=false
-endif
-
-endif # CC2592_USE_HGM
-
-endif # CC2592
-
-ifneq ($(CC2538_RECEIVE_SENSITIVITY),)
-COMMONCFLAGS += -DOPENTHREAD_CONFIG_CC2538_RECEIVE_SENSITIVITY=$(CC2538_RECEIVE_SENSITIVITY)
-endif
-
-ifneq ($(CC2538_RSSI_OFFSET),)
-COMMONCFLAGS += -DOPENTHREAD_CONFIG_CC2538_RSSI_OFFSET=$(CC2538_RSSI_OFFSET)
-endif
-
-CPPFLAGS                       += \
-    $(COMMONCFLAGS)               \
-    $(target_CPPFLAGS)            \
-    $(NULL)
-
-CFLAGS                         += \
-    $(COMMONCFLAGS)               \
-    $(target_CFLAGS)              \
-    $(NULL)
-
-CXXFLAGS                       += \
-    $(COMMONCFLAGS)               \
-    $(target_CXXFLAGS)            \
-    -fno-exceptions               \
-    -fno-rtti                     \
-    $(NULL)
-
-LDFLAGS                        += \
-    $(COMMONCFLAGS)               \
-    $(target_LDFLAGS)             \
-    -nostartfiles                 \
-    -specs=nano.specs             \
-    -specs=nosys.specs            \
-    -Wl,--gc-sections             \
-    $(NULL)
-
-ECHO                            := @echo
-MAKE                            := make
-MKDIR_P                         := mkdir -p
-LN_S                            := ln -s
-RM_F                            := rm -f
-
-INSTALL                         := /usr/bin/install
-INSTALLFLAGS                    := -p
-
-BuildPath                       = build
-TopBuildDir                     = $(BuildPath)
-AbsTopBuildDir                  = $(PWD)/$(TopBuildDir)
-
-ResultPath                      = output
-TopResultDir                    = $(ResultPath)
-AbsTopResultDir                 = $(PWD)/$(TopResultDir)
-
-TargetTuple                     = cc2538
-
-ARCHS                           = cortex-m3
-
-TopTargetLibDir                 = $(TopResultDir)/$(TargetTuple)/lib
-
-ifndef BuildJobs
-BuildJobs := $(shell getconf _NPROCESSORS_ONLN)
-endif
-JOBSFLAG := -j$(BuildJobs)
-
-#
-# configure-arch <arch>
-#
-# Configure OpenThread for the specified architecture.
-#
-#   arch - The architecture to configure.
-#
-define configure-arch
-$(ECHO) "  CONFIG   $(TargetTuple)..."
-(cd $(BuildPath)/$(TargetTuple) && $(AbsTopSourceDir)/configure \
-INSTALL="$(INSTALL) $(INSTALLFLAGS)" \
-CPP="$(CPP)" CC="$(CC)" CXX="$(CXX)" OBJC="$(OBJC)" OBJCXX="$(OBJCXX)" AR="$(AR)" RANLIB="$(RANLIB)" NM="$(NM)" STRIP="$(STRIP)" CPPFLAGS="$(CPPFLAGS)" CFLAGS="$(CFLAGS)" CXXFLAGS="$(CXXFLAGS)" LDFLAGS="$(LDFLAGS)" \
---host=arm-none-eabi \
---prefix=/ \
---exec-prefix=/$(TargetTuple) \
-$(configure_OPTIONS))
-endef # configure-arch
-
-#
-# build-arch <arch>
-#
-# Build the OpenThread intermediate build products for the specified
-# architecture.
-#
-#   arch - The architecture to build.
-#
-define build-arch
-$(ECHO) "  BUILD    $(TargetTuple)"
-$(MAKE) $(JOBSFLAG) -C $(BuildPath)/$(TargetTuple) --no-print-directory \
-all
-endef # build-arch
-
-#
-# stage-arch <arch>
-#
-# Stage (install) the OpenThread final build products for the specified
-# architecture.
-#
-#   arch - The architecture to stage.
-#
-define stage-arch
-$(ECHO) "  STAGE    $(TargetTuple)"
-$(MAKE) $(JOBSFLAG) -C $(BuildPath)/$(TargetTuple) --no-print-directory \
-DESTDIR=$(AbsTopResultDir) \
-install
-endef # stage-arch
-
-#
-# ARCH_template <arch>
-#
-# Define macros, targets and rules to configure, build, and stage the
-# OpenThread for a single architecture.
-#
-#   arch - The architecture to instantiate the template for.
-#
-define ARCH_template
-CONFIGURE_TARGETS += configure-$(1)
-BUILD_TARGETS     += do-build-$(1)
-STAGE_TARGETS     += stage-$(1)
-BUILD_DIRS        += $(BuildPath)/$(TargetTuple)
-DIRECTORIES       += $(BuildPath)/$(TargetTuple)
-
-configure-$(1): target_CPPFLAGS=$($(1)_target_CPPFLAGS)
-configure-$(1): target_CFLAGS=$($(1)_target_CFLAGS)
-configure-$(1): target_CXXFLAGS=$($(1)_target_CXXFLAGS)
-configure-$(1): target_LDFLAGS=$($(1)_target_LDFLAGS)
-
-configure-$(1): $(BuildPath)/$(TargetTuple)/config.status
-
-$(BuildPath)/$(TargetTuple)/config.status: | $(BuildPath)/$(TargetTuple)
-	$$(call configure-arch,$(1))
-
-do-build-$(1): configure-$(1)
-
-do-build-$(1):
-	+$$(call build-arch,$(1))
-
-stage-$(1): do-build-$(1)
-
-stage-$(1): | $(TopResultDir)
-	$$(call stage-arch,$(1))
-
-$(1): stage-$(1)
-endef # ARCH_template
-
-.DEFAULT_GOAL := all
-
-all: stage
-
-#
-# cortex-m3
-#
-
-cortex-m3_target_ABI                  = cortex-m3
-cortex-m3_target_CPPFLAGS             = -mcpu=cortex-m3 -mfloat-abi=soft -mthumb
-cortex-m3_target_CFLAGS               = -mcpu=cortex-m3 -mfloat-abi=soft -mthumb
-cortex-m3_target_CXXFLAGS             = -mcpu=cortex-m3 -mfloat-abi=soft -mthumb
-cortex-m3_target_LDFLAGS              = -mcpu=cortex-m3 -mfloat-abi=soft -mthumb
-
-# Instantiate an architecture-specific build template for each target
-# architecture.
-
-$(foreach arch,$(ARCHS),$(eval $(call ARCH_template,$(arch))))
-
-#
-# Common / Finalization
-#
-
-configure: $(CONFIGURE_TARGETS)
-
-build: $(BUILD_TARGETS)
-
-stage: $(STAGE_TARGETS)
-
-DIRECTORIES     = $(TopResultDir) $(TopResultDir)/$(TargetTuple)/lib $(BUILD_DIRS)
-
-CLEAN_DIRS      = $(TopResultDir) $(BUILD_DIRS)
-
-all: stage
-
-$(DIRECTORIES):
-	$(ECHO) "  MKDIR    $@"
-	@$(MKDIR_P) "$@"
-
-clean:
-	$(ECHO) "  CLEAN"
-	@$(RM_F) -r $(CLEAN_DIRS)
-
-help:
-	$(ECHO) "Simply type 'make -f $(firstword $(MAKEFILE_LIST))' to build OpenThread for the following "
-	$(ECHO) "architectures: "
-	$(ECHO) ""
-	$(ECHO) "    $(ARCHS)"
-	$(ECHO) ""
-	$(ECHO) "To build only a particular architecture, specify: "
-	$(ECHO) ""
-	$(ECHO) "    make -f $(firstword $(MAKEFILE_LIST)) <architecture>"
-	$(ECHO) ""
diff --git a/examples/Makefile-simulation b/examples/Makefile-simulation
index f16ce89..72894fb 100644
--- a/examples/Makefile-simulation
+++ b/examples/Makefile-simulation
@@ -45,7 +45,6 @@
 COMMISSIONER                   ?= 1
 CHANNEL_MANAGER                ?= 1
 CHANNEL_MONITOR                ?= 1
-CHILD_SUPERVISION              ?= 1
 DATASET_UPDATER                ?= 1
 DHCP6_CLIENT                   ?= 1
 DHCP6_SERVER                   ?= 1
@@ -58,12 +57,11 @@
 IP6_FRAGM                      ?= 1
 JAM_DETECTION                  ?= 1
 JOINER                         ?= 1
-LEGACY                         ?= 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/examples/README.md b/examples/README.md
index 2d0f5d5..a7d1acd 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -6,17 +6,16 @@
 
 | Makefile switch | CMake switch | Description |
 | --- | --- | --- |
+|  | OT_15_4 | Enables 802.15.4 radio link. |
 | ANYCAST_LOCATOR | OT_ANYCAST_LOCATOR | Enables anycast locator functionality. |
 | BACKBONE_ROUTER | OT_BACKBONE_ROUTER | Enables Backbone Router functionality for Thread 1.2. |
 | BIG_ENDIAN | OT_BIG_ENDIAN | Allows the host platform to use big-endian byte order. |
 | BORDER_AGENT | OT_BORDER_AGENT | Enables support for border agent. In most cases, enable this switch if you are building On-mesh Commissioner or Border Router with External Commissioning support. |
 | BORDER_ROUTER | OT_BORDER_ROUTER | Enables support for Border Router. This switch is usually combined with the BORDER_AGENT and UDP_FORWARD (or PLATFORM_UDP in case of RCP design) switches to build Border Router device. |
 | BORDER_ROUTING | OT_BORDER_ROUTING | Enables bi-directional border routing between Thread and Infrastructure networks for Border Router. |
-| BORDER_ROUTING_NAT64 | OT_BORDER_ROUTING_NAT64 | Enables NAT64 border routing support for Border Router. |
 | BUILTIN_MBEDTLS_MANAGEMENT | OT_BUILTIN_MBEDTLS_MANAGEMENT | Enables the built-in mbedTLS management. Enable this switch if the external mbedTLS is used, but mbedTLS memory allocation and debug config should be managed internally by OpenThread. |
 | CHANNEL_MANAGER | OT_CHANNEL_MANAGER | Enables support for channel manager. Enable this switch on devices that are supposed to request a Thread network channel change. This switch should be used only with an FTD build. |
 | CHANNEL_MONITOR | OT_CHANNEL_MONITOR | Enables support for channel monitor. Enable this switch on devices that are supposed to determine the cleaner channels. |
-| CHILD_SUPERVISION | OT_CHILD_SUPERVISION | Enables support for [child supervision](https://openthread.io/guides/build/features/child-supervision). Enable this switch on a parent or child node with custom OpenThread application that manages the supervision, checks timeout intervals, and verifies connectivity between parent and child. |
 | COAP | OT_COAP | Enables support for the CoAP API. Enable this switch if you want to control Constrained Application Protocol communication. |
 | COAP_OBSERVE | OT_COAP_OBSERVE | Enables support for CoAP Observe (RFC7641) API. |
 | COAPS | OT_COAPS | Enables support for the secure CoAP API. Enable this switch if you want to control Constrained Application Protocol Secure (CoAP over DTLS) communication. |
@@ -35,26 +34,27 @@
 | DEBUG_UART_LOG | not implemented | Enables the log output for the debug UART. Requires OPENTHREAD_CONFIG_ENABLE_DEBUG_UART to be enabled. |
 | DNS_CLIENT | OT_DNS_CLIENT | Enables support for DNS client. Enable this switch on a device that sends a DNS query for AAAA (IPv6) record. |
 | DNS_DSO | OT_DNS_DSO | Enables support for DNS Stateful Operations (DSO). |
+| DNS_UPSTREAM_QUERY | OT_DNS_UPSTREAM_QUERY | Enables forwarding DNS queries to upstream DNS server. |
 | DNSSD_SERVER | OT_DNSSD_SERVER | Enables support for DNS-SD server. DNS-SD server use service information from local SRP server to resolve DNS-SD query questions. |
 | DUA | OT_DUA | Enables the Domain Unicast Address feature for Thread 1.2. |
 | DYNAMIC_LOG_LEVEL | not implemented | Enables the dynamic log level feature. Enable this switch if OpenThread log level is required to be set at runtime. See [Logging guide](https://openthread.io/guides/build/logs) to learn more. |
 | ECDSA | OT_ECDSA | Enables support for Elliptic Curve Digital Signature Algorithm. Enable this switch if ECDSA digital signature is used by application. |
-| EXCLUDE_TCPLP_LIB | OT_EXCLUDE_TCPLP_LIB | Exclude TCPlp library from the build. |
 | EXTERNAL_HEAP | OT_EXTERNAL_HEAP | Enables support for external heap. Enable this switch if the platform uses its own heap. Make sure to specify the external heap Calloc and Free functions to be used by the OpenThread stack. |
 | FULL_LOGS | OT_FULL_LOGS | Enables all log levels and regions. This switch sets the log level to OT_LOG_LEVEL_DEBG and turns on all region flags. See [Logging guide](https://openthread.io/guides/build/logs) to learn more. |
 | HISTORY_TRACKER | OT_HISTORY_TRACKER | Enables support for History Tracker. |
 | IP6_FRAGM | OT_IP6_FRAGM | Enables support for IPv6 fragmentation. |
 | JAM_DETECTION | OT_JAM_DETECTION | Enables support for [Jam Detection](https://openthread.io/guides/build/features/jam-detection). Enable this switch if a device requires the ability to detect signal jamming on a specific channel. |
 | JOINER | OT_JOINER | Enables [support for Joiner](https://openthread.io/reference/group/api-joiner). Enable this switch on a device that has to be commissioned to join the network. |
-| LEGACY | OT_LEGACY | Enables support for legacy network. |
 | LINK_RAW | OT_LINK_RAW | Enables the Link Raw service. |
 | LOG_OUTPUT | not implemented | Defines if the LOG output is to be created and where it goes. There are several options available: `NONE`, `DEBUG_UART`, `APP`, `PLATFORM_DEFINED` (default). See [Logging guide](https://openthread.io/guides/build/logs) to learn more. |
 | MAC_FILTER | OT_MAC_FILTER | Enables support for the MAC filter. |
 | MLE_LONG_ROUTES | OT_MLE_LONG_ROUTES | Enables the MLE long routes extension. **Note: Enabling this feature breaks conformance to the Thread Specification.** |
 | MLR | OT_MLR | Enables Multicast Listener Registration feature for Thread 1.2. |
-| MTD_NETDIAG | OT_MTD_NETDIAG | Enables the TMF network diagnostics on MTDs. |
 | MULTIPLE_INSTANCE | OT_MULTIPLE_INSTANCE | Enables multiple OpenThread instances. |
+| NAT64_BORDER_ROUTING | OT_NAT64_BORDER_ROUTING | Enables NAT64 border routing support for Border Router. |
+| NAT64_TRANSLATOR | OT_NAT64_TRANSLATOR | Enables NAT64 translator for Border Router. |
 | NETDATA_PUBLISHER | OT_NETDATA_PUBLISHER | Enables support for Thread Network Data publisher. |
+| NETDIAG_CLIENT | OT_NETDIAG_CLIENT | Enables Network Diagnostics client functionality. |
 | PING_SENDER | OT_PING_SENDER | Enables support for ping sender. |
 | OTNS | OT_OTNS | Enables support for [OpenThread Network Simulator](https://github.com/openthread/ot-ns). Enable this switch if you are building OpenThread for OpenThread Network Simulator. |
 | PLATFORM_UDP | OT_PLATFORM_UDP | Enables platform UDP support. |
@@ -66,8 +66,15 @@
 | SPINEL_ENCRYPTER_LIBS | not implemented | Specifies library files (absolute paths) for implementing the NCP Spinel Encrypter. |
 | SRP_CLIENT | OT_SRP_CLIENT | Enable support for SRP client. |
 | SRP_SERVER | OT_SRP_SERVER | Enable support for SRP server. |
+| TCP | OT_TCP | Enable support for TCP (based on TCPlp). |
 | THREAD_VERSION | OT_THREAD_VERSION | Enables the chosen Thread version (1.1 / 1.2 (default)). For example, set to `1.1` for Thread 1.1. |
-| TIME_SYNC | OT_TIME_SYNC | Enables the time synchronization service feature. **Note: Enabling this feature breaks conformance to the Thread Specification.** |  |
+| TIME_SYNC | OT_TIME_SYNC | Enables the time synchronization service feature. **Note: Enabling this feature breaks conformance to the Thread Specification.** |
 | TREL | OT_TREL | Enables TREL radio link for Thread over Infrastructure feature. |
-| UDP_FORWARD | OT_UDP_FORWARD | Enables support for UDP forward. | Enable this switch on the Border Router device (running on the NCP design) with External Commissioning support to service Thread Commissioner packets on the NCP side. |
+| UDP_FORWARD | OT_UDP_FORWARD | Enables support for UDP forward. Enable this switch on the Border Router device (running on the NCP design) with External Commissioning support to service Thread Commissioner packets on the NCP side. |
 | UPTIME | OT_UPTIME | Enables support for tracking OpenThread instance's uptime. |
+
+Removed or replaced switches:
+
+| Makefile switch | CMake switch | Description |
+| --- | --- | --- |
+| MTD_NETDIAG | OT_MTD_NETDIAG | Use NEDIAG_CLIENT to enable client functionality. Server functionality is always supported. |
diff --git a/examples/apps/cli/cli_uart.cpp b/examples/apps/cli/cli_uart.cpp
index b7fa502..81f05f8 100644
--- a/examples/apps/cli/cli_uart.cpp
+++ b/examples/apps/cli/cli_uart.cpp
@@ -137,7 +137,7 @@
     static const char sEraseString[] = {'\b', ' ', '\b'};
     static const char CRNL[]         = {'\r', '\n'};
     static uint8_t    sLastChar      = '\0';
-    const uint8_t *   end;
+    const uint8_t    *end;
 
     end = aBuf + aBufLength;
 
@@ -368,15 +368,9 @@
     return rval;
 }
 
-void otPlatUartReceived(const uint8_t *aBuf, uint16_t aBufLength)
-{
-    ReceiveTask(aBuf, aBufLength);
-}
+void otPlatUartReceived(const uint8_t *aBuf, uint16_t aBufLength) { ReceiveTask(aBuf, aBufLength); }
 
-void otPlatUartSendDone(void)
-{
-    SendDoneTask();
-}
+void otPlatUartSendDone(void) { SendDoneTask(); }
 
 extern "C" void otAppCliInit(otInstance *aInstance)
 {
diff --git a/examples/apps/cli/ftd.cmake b/examples/apps/cli/ftd.cmake
index f5a2870..9ed9be1 100644
--- a/examples/apps/cli/ftd.cmake
+++ b/examples/apps/cli/ftd.cmake
@@ -44,8 +44,17 @@
     ${OT_PLATFORM_LIB_FTD}
     openthread-cli-ftd
     ${OT_MBEDTLS}
+    ot-config-ftd
     ot-config
 )
 
+if(OT_LINKER_MAP)
+    if("${CMAKE_CXX_COMPILER_ID}" MATCHES "AppleClang")
+        target_link_libraries(ot-cli-ftd PRIVATE -Wl,-map,ot-cli-ftd.map)
+    else()
+        target_link_libraries(ot-cli-ftd PRIVATE -Wl,-Map=ot-cli-ftd.map)
+    endif()
+endif()
+
 install(TARGETS ot-cli-ftd
     DESTINATION bin)
diff --git a/examples/apps/cli/main.c b/examples/apps/cli/main.c
index 165f32f..cc52d9a 100644
--- a/examples/apps/cli/main.c
+++ b/examples/apps/cli/main.c
@@ -50,24 +50,15 @@
 extern void otAppCliInit(otInstance *aInstance);
 
 #if OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
-void *otPlatCAlloc(size_t aNum, size_t aSize)
-{
-    return calloc(aNum, aSize);
-}
+OT_TOOL_WEAK void *otPlatCAlloc(size_t aNum, size_t aSize) { return calloc(aNum, aSize); }
 
-void otPlatFree(void *aPtr)
-{
-    free(aPtr);
-}
+OT_TOOL_WEAK void otPlatFree(void *aPtr) { free(aPtr); }
 #endif
 
-void otTaskletsSignalPending(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
+void otTaskletsSignalPending(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
 
 #if OPENTHREAD_POSIX && !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
-static void ProcessExit(void *aContext, uint8_t aArgsLength, char *aArgs[])
+static otError ProcessExit(void *aContext, uint8_t aArgsLength, char *aArgs[])
 {
     OT_UNUSED_VARIABLE(aContext);
     OT_UNUSED_VARIABLE(aArgsLength);
@@ -75,9 +66,33 @@
 
     exit(EXIT_SUCCESS);
 }
-static const otCliCommand kCommands[] = {{"exit", ProcessExit}};
+
+#if OPENTHREAD_EXAMPLES_SIMULATION
+extern otError ProcessNodeIdFilter(void *aContext, uint8_t aArgsLength, char *aArgs[]);
 #endif
 
+static const otCliCommand kCommands[] = {
+    {"exit", ProcessExit},
+#if OPENTHREAD_EXAMPLES_SIMULATION
+    /*
+     * The CLI command `nodeidfilter` only works for simulation in real time.
+     *
+     * It can be used either as an allow list or a deny list. Once the filter is cleared, the first `nodeidfilter allow`
+     * or `nodeidfilter deny` will determine whether it is set up as an allow or deny list. Subsequent calls should
+     * use the same sub-command to add new node IDs, e.g., if we first call `nodeidfilter allow` (which sets the filter
+     * up  as an allow list), a subsequent `nodeidfilter deny` will result in `InvalidState` error.
+     *
+     * The usage of the command `nodeidfilter`:
+     *     - `nodeidfilter deny <nodeid>` :  It denies the connection to a specified node (use as deny-list).
+     *     - `nodeidfilter allow <nodeid> :  It allows the connection to a specified node (use as allow-list).
+     *     - `nodeidfilter clear`         :  It restores the filter state to default.
+     *     - `nodeidfilter`               :  Outputs filter mode (allow-list or deny-list) and filtered node IDs.
+     */
+    {"nodeidfilter", ProcessNodeIdFilter},
+#endif
+};
+#endif // OPENTHREAD_POSIX && !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
+
 int main(int argc, char *argv[])
 {
     otInstance *instance;
@@ -111,7 +126,7 @@
     otAppCliInit(instance);
 
 #if OPENTHREAD_POSIX && !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
-    otCliSetUserCommands(kCommands, OT_ARRAY_LENGTH(kCommands), instance);
+    IgnoreError(otCliSetUserCommands(kCommands, OT_ARRAY_LENGTH(kCommands), instance));
 #endif
 
     while (!otSysPseudoResetWasRequested())
diff --git a/examples/apps/cli/mtd.cmake b/examples/apps/cli/mtd.cmake
index 7b71883..786f741 100644
--- a/examples/apps/cli/mtd.cmake
+++ b/examples/apps/cli/mtd.cmake
@@ -44,8 +44,17 @@
     ${OT_PLATFORM_LIB_MTD}
     openthread-cli-mtd
     ${OT_MBEDTLS}
+    ot-config-mtd
     ot-config
 )
 
+if(OT_LINKER_MAP)
+    if("${CMAKE_CXX_COMPILER_ID}" MATCHES "AppleClang")
+        target_link_libraries(ot-cli-mtd PRIVATE -Wl,-map,ot-cli-mtd.map)
+    else()
+        target_link_libraries(ot-cli-mtd PRIVATE -Wl,-Map=ot-cli-mtd.map)
+    endif()
+endif()
+
 install(TARGETS ot-cli-mtd
     DESTINATION bin)
diff --git a/examples/apps/cli/radio.cmake b/examples/apps/cli/radio.cmake
index 4302adc..b2fe5d9 100644
--- a/examples/apps/cli/radio.cmake
+++ b/examples/apps/cli/radio.cmake
@@ -48,9 +48,18 @@
     ${OT_PLATFORM_LIB_RCP}
     openthread-cli-radio
     ${OT_MBEDTLS_RCP}
+    ot-config-radio
     ot-config
 )
 
+if(OT_LINKER_MAP)
+    if("${CMAKE_CXX_COMPILER_ID}" MATCHES "AppleClang")
+        target_link_libraries(ot-cli-radio PRIVATE -Wl,-map,ot-cli-radio.map)
+    else()
+        target_link_libraries(ot-cli-radio PRIVATE -Wl,-Map=ot-cli-radio.map)
+    endif()
+endif()
+
 install(TARGETS ot-cli-radio
     DESTINATION bin
 )
diff --git a/examples/apps/ncp/ftd.cmake b/examples/apps/ncp/ftd.cmake
index c7d0bbf..a7ffa51 100644
--- a/examples/apps/ncp/ftd.cmake
+++ b/examples/apps/ncp/ftd.cmake
@@ -44,7 +44,16 @@
     ${OT_PLATFORM_LIB_FTD}
     openthread-ncp-ftd
     ${OT_MBEDTLS}
+    ot-config-ftd
     ot-config
 )
 
+if(OT_LINKER_MAP)
+    if("${CMAKE_CXX_COMPILER_ID}" MATCHES "AppleClang")
+        target_link_libraries(ot-ncp-ftd PRIVATE -Wl,-map,ot-ncp-ftd.map)
+    else()
+        target_link_libraries(ot-ncp-ftd PRIVATE -Wl,-Map=ot-ncp-ftd.map)
+    endif()
+endif()
+
 install(TARGETS ot-ncp-ftd DESTINATION bin)
diff --git a/examples/apps/ncp/main.c b/examples/apps/ncp/main.c
index af50d14..9c21c57 100644
--- a/examples/apps/ncp/main.c
+++ b/examples/apps/ncp/main.c
@@ -46,21 +46,12 @@
 extern void otAppNcpInit(otInstance *aInstance);
 
 #if OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
-void *otPlatCAlloc(size_t aNum, size_t aSize)
-{
-    return calloc(aNum, aSize);
-}
+OT_TOOL_WEAK void *otPlatCAlloc(size_t aNum, size_t aSize) { return calloc(aNum, aSize); }
 
-void otPlatFree(void *aPtr)
-{
-    free(aPtr);
-}
+OT_TOOL_WEAK void otPlatFree(void *aPtr) { free(aPtr); }
 #endif
 
-void otTaskletsSignalPending(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
+void otTaskletsSignalPending(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
 
 int main(int argc, char *argv[])
 {
diff --git a/examples/apps/ncp/mtd.cmake b/examples/apps/ncp/mtd.cmake
index 61380c3..2fecbac 100644
--- a/examples/apps/ncp/mtd.cmake
+++ b/examples/apps/ncp/mtd.cmake
@@ -44,7 +44,16 @@
     ${OT_PLATFORM_LIB_MTD}
     openthread-ncp-mtd
     ${OT_MBEDTLS}
+    ot-config-mtd
     ot-config
 )
 
+if(OT_LINKER_MAP)
+    if("${CMAKE_CXX_COMPILER_ID}" MATCHES "AppleClang")
+        target_link_libraries(ot-ncp-mtd PRIVATE -Wl,-map,ot-ncp-mtd.map)
+    else()
+        target_link_libraries(ot-ncp-mtd PRIVATE -Wl,-Map=ot-ncp-mtd.map)
+    endif()
+endif()
+
 install(TARGETS ot-ncp-mtd DESTINATION bin)
diff --git a/examples/apps/ncp/ncp.c b/examples/apps/ncp/ncp.c
index ee7ccf7..df6529a 100644
--- a/examples/apps/ncp/ncp.c
+++ b/examples/apps/ncp/ncp.c
@@ -37,15 +37,9 @@
 #if !OPENTHREAD_CONFIG_NCP_SPI_ENABLE
 #include "utils/uart.h"
 
-void otPlatUartReceived(const uint8_t *aBuf, uint16_t aBufLength)
-{
-    otNcpHdlcReceive(aBuf, aBufLength);
-}
+void otPlatUartReceived(const uint8_t *aBuf, uint16_t aBufLength) { otNcpHdlcReceive(aBuf, aBufLength); }
 
-void otPlatUartSendDone(void)
-{
-    otNcpHdlcSendDone();
-}
+void otPlatUartSendDone(void) { otNcpHdlcSendDone(); }
 #endif
 
 #if !OPENTHREAD_ENABLE_NCP_VENDOR_HOOK
diff --git a/examples/apps/ncp/rcp.cmake b/examples/apps/ncp/rcp.cmake
index 7ba3b97..0f8be33 100644
--- a/examples/apps/ncp/rcp.cmake
+++ b/examples/apps/ncp/rcp.cmake
@@ -43,7 +43,16 @@
     openthread-radio
     ${OT_PLATFORM_LIB_RCP}
     openthread-rcp
+    ot-config-radio
     ot-config
 )
 
+if(OT_LINKER_MAP)
+    if("${CMAKE_CXX_COMPILER_ID}" MATCHES "AppleClang")
+        target_link_libraries(ot-rcp PRIVATE -Wl,-map,ot-rcp.map)
+    else()
+        target_link_libraries(ot-rcp PRIVATE -Wl,-Map=ot-rcp.map)
+    endif()
+endif()
+
 install(TARGETS ot-rcp DESTINATION bin)
diff --git a/examples/common-switches.mk b/examples/common-switches.mk
index ac05ccd..36ff957 100644
--- a/examples/common-switches.mk
+++ b/examples/common-switches.mk
@@ -34,7 +34,6 @@
 BORDER_AGENT              ?= 0
 BORDER_ROUTER             ?= 0
 BORDER_ROUTING            ?= 0
-BORDER_ROUTING_NAT64	  ?= 0
 COAP                      ?= 0
 COAP_BLOCK                ?= 0
 COAP_OBSERVE              ?= 0
@@ -43,7 +42,6 @@
 COVERAGE                  ?= 0
 CHANNEL_MANAGER           ?= 0
 CHANNEL_MONITOR           ?= 0
-CHILD_SUPERVISION         ?= 0
 DATASET_UPDATER           ?= 0
 DEBUG                     ?= 0
 DHCP6_CLIENT              ?= 0
@@ -53,6 +51,7 @@
 DISABLE_TOOLS             ?= 0
 DNS_CLIENT                ?= 0
 DNS_DSO                   ?= 0
+DNS_UPSTREAM_QUERY        ?= 0
 DNSSD_SERVER              ?= 0
 DUA                       ?= 0
 DYNAMIC_LOG_LEVEL         ?= 0
@@ -62,19 +61,22 @@
 IP6_FRAGM                 ?= 0
 JAM_DETECTION             ?= 0
 JOINER                    ?= 0
-LEGACY                    ?= 0
 ifeq ($(REFERENCE_DEVICE),1)
 LOG_OUTPUT                ?= APP
 endif
 LINK_RAW                  ?= 0
 MAC_FILTER                ?= 0
+MESH_DIAG                 ?= 0
 MESSAGE_USE_HEAP          ?= 0
 MLE_LONG_ROUTES           ?= 0
 MLR                       ?= 0
 MTD_NETDIAG               ?= 0
 MULTIPLE_INSTANCE         ?= 0
+NAT64_BORDER_ROUTING      ?= 0
+NAT64_TRANSLATOR          ?= 0
 NEIGHBOR_DISCOVERY_AGENT  ?= 0
 NETDATA_PUBLISHER         ?= 0
+NETDIAG_CLIENT            ?= 0
 OTNS                      ?= 0
 PING_SENDER               ?= 1
 PLATFORM_UDP              ?= 0
@@ -86,6 +88,7 @@
 SNTP_CLIENT               ?= 0
 SRP_CLIENT                ?= 0
 SRP_SERVER                ?= 0
+TCP                       ?= 0
 THREAD_VERSION            ?= 1.3
 TIME_SYNC                 ?= 0
 TREL                      ?= 0
@@ -117,8 +120,12 @@
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE=1
 endif
 
-ifeq ($(BORDER_ROUTING_NAT64),1)
-COMMONCFLAGS		       += -DOPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE=1
+ifeq ($(NAT64_BORDER_ROUTING),1)
+COMMONCFLAGS		       += -DOPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE=1
+endif
+
+ifeq ($(NAT64_TRANSLATOR),1)
+COMMONCFLAGS		       += -DOPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE=1
 endif
 
 ifeq ($(COAP),1)
@@ -153,10 +160,6 @@
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE=1
 endif
 
-ifeq ($(CHILD_SUPERVISION),1)
-COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE=1
-endif
-
 ifeq ($(CSL_RECEIVER),1)
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE=1
 endif
@@ -207,6 +210,10 @@
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_DNS_DSO_ENABLE=1
 endif
 
+ifeq ($(DNS_UPSTREAM_QUERY), 1)
+COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE=1
+endif
+
 ifeq ($(DNSSD_SERVER),1)
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE=1
 endif
@@ -243,10 +250,6 @@
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_JOINER_ENABLE=1
 endif
 
-ifeq ($(LEGACY),1)
-COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_LEGACY_ENABLE=1
-endif
-
 ifeq ($(LINK_RAW),1)
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_LINK_RAW_ENABLE=1
 endif
@@ -267,6 +270,10 @@
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_MAC_FILTER_ENABLE=1
 endif
 
+ifeq ($(MESH_DIAG),1)
+COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_MESH_DIAG_ENABLE=1
+endif
+
 ifeq ($(MESSAGE_USE_HEAP),1)
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_MESSAGE_USE_HEAP_ENABLE=1
 endif
@@ -280,6 +287,9 @@
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_MLR_ENABLE=1
 endif
 
+# This config is removed but we still check and add the
+# `OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE` so to
+# get an error during build if `MTD_NETDIAG` is used.
 ifeq ($(MTD_NETDIAG),1)
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE=1
 endif
@@ -296,6 +306,10 @@
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE=1
 endif
 
+ifeq ($(NETDIAG_CLIENT),1)
+COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE=1
+endif
+
 ifeq ($(PING_SENDER),1)
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_PING_SENDER_ENABLE=1
 endif
@@ -329,12 +343,18 @@
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_SRP_SERVER_ENABLE=1
 endif
 
+ifeq ($(TCP),1)
+COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_TCP_ENABLE=1
+endif
+
 ifeq ($(THREAD_VERSION),1.1)
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_THREAD_VERSION=2
 else ifeq ($(THREAD_VERSION),1.2)
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_THREAD_VERSION=3
 else ifeq ($(THREAD_VERSION),1.3)
 COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_THREAD_VERSION=4
+else ifeq ($(THREAD_VERSION),1.3.1)
+COMMONCFLAGS                   += -DOPENTHREAD_CONFIG_THREAD_VERSION=5
 endif
 
 ifeq ($(TIME_SYNC),1)
diff --git a/examples/platforms/Makefile.am b/examples/platforms/Makefile.am
index e7bd3ed..7f86919 100644
--- a/examples/platforms/Makefile.am
+++ b/examples/platforms/Makefile.am
@@ -30,6 +30,7 @@
 
 EXTRA_DIST                              = \
     cc1352                                \
+    cc2538                                \
     cc2652                                \
     efr32                                 \
     gp712                                 \
@@ -45,7 +46,6 @@
 # Always package (e.g. for 'make dist') these subdirectories.
 
 DIST_SUBDIRS                            = \
-    cc2538                                \
     simulation                            \
     utils                                 \
     $(NULL)
@@ -56,10 +56,6 @@
     utils                                 \
     $(NULL)
 
-if OPENTHREAD_PLATFORM_CC2538
-SUBDIRS                                += cc2538
-endif
-
 if OPENTHREAD_PLATFORM_SIMULATION
 SUBDIRS                                += simulation
 endif
diff --git a/examples/platforms/Makefile.platform.am b/examples/platforms/Makefile.platform.am
index 8ab6520..71b82b0 100644
--- a/examples/platforms/Makefile.platform.am
+++ b/examples/platforms/Makefile.platform.am
@@ -41,10 +41,6 @@
 SOURCES_COMMON      = $(NULL)
 LIBTOOLFLAGS_COMMON = --preserve-dup-deps
 
-if OPENTHREAD_EXAMPLES_CC2538
-include $(top_srcdir)/examples/platforms/cc2538/Makefile.platform.am
-endif
-
 if OPENTHREAD_EXAMPLES_SIMULATION
 include $(top_srcdir)/examples/platforms/simulation/Makefile.platform.am
 endif
diff --git a/examples/platforms/cc2538/CMakeLists.txt b/examples/platforms/cc2538/CMakeLists.txt
deleted file mode 100644
index f8ed5f2..0000000
--- a/examples/platforms/cc2538/CMakeLists.txt
+++ /dev/null
@@ -1,79 +0,0 @@
-#
-#  Copyright (c) 2019, 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_PLATFORM_LIB "openthread-cc2538" PARENT_SCOPE)
-
-if(NOT OT_CONFIG)
-    set(OT_CONFIG "openthread-core-cc2538-config.h")
-    set(OT_CONFIG ${OT_CONFIG} PARENT_SCOPE)
-endif()
-
-list(APPEND OT_PLATFORM_DEFINES
-    "OPENTHREAD_CORE_CONFIG_PLATFORM_CHECK_FILE=\"openthread-core-cc2538-config-check.h\""
-    "OPENTHREAD_CONFIG_NCP_HDLC_ENABLE=1"
-)
-set(OT_PLATFORM_DEFINES ${OT_PLATFORM_DEFINES} PARENT_SCOPE)
-
-list(APPEND OT_PLATFORM_DEFINES "OPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"${OT_CONFIG}\"")
-
-add_library(openthread-cc2538
-    alarm.c
-    diag.c
-    entropy.c
-    flash.c
-    misc.c
-    radio.c
-    startup-gcc.c
-    system.c
-    logging.c
-    uart.c
-    $<TARGET_OBJECTS:openthread-platform-utils>
-)
-
-target_link_libraries(openthread-cc2538
-    PRIVATE
-        ot-config
-    PUBLIC
-        -T${PROJECT_SOURCE_DIR}/examples/platforms/cc2538/cc2538.ld
-        -Wl,--gc-sections -Wl,-Map=$<TARGET_PROPERTY:NAME>.map
-)
-
-target_compile_definitions(openthread-cc2538
-    PUBLIC
-        ${OT_PLATFORM_DEFINES}
-)
-
-target_compile_options(openthread-cc2538 PRIVATE
-    ${OT_CFLAGS}
-)
-
-target_include_directories(openthread-cc2538 PRIVATE
-    ${OT_PUBLIC_INCLUDES}
-    ${PROJECT_SOURCE_DIR}/examples/platforms
-    ${PROJECT_SOURCE_DIR}/src/core
-)
diff --git a/examples/platforms/cc2538/Makefile.am b/examples/platforms/cc2538/Makefile.am
deleted file mode 100644
index fb802f2..0000000
--- a/examples/platforms/cc2538/Makefile.am
+++ /dev/null
@@ -1,69 +0,0 @@
-#
-#  Copyright (c) 2016, 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
-
-# Do not enable -Wcast-align for this platform
-override CFLAGS    := $(filter-out -Wcast-align,$(CFLAGS))
-override CXXFLAGS  := $(filter-out -Wcast-align,$(CXXFLAGS))
-
-lib_LIBRARIES                             = libopenthread-cc2538.a
-
-libopenthread_cc2538_a_CPPFLAGS           = \
-    -I$(top_srcdir)/include                 \
-    -I$(top_srcdir)/examples/platforms      \
-    -I$(top_srcdir)/src/core                \
-    $(NULL)
-
-PLATFORM_SOURCES                          = \
-    alarm.c                                 \
-    cc2538-reg.h                            \
-    diag.c                                  \
-    entropy.c                               \
-    flash.c                                 \
-    misc.c                                  \
-    openthread-core-cc2538-config.h         \
-    openthread-core-cc2538-config-check.h   \
-    platform-cc2538.h                       \
-    radio.c                                 \
-    rom-utility.h                           \
-    startup-gcc.c                           \
-    system.c                                \
-    logging.c                               \
-    uart.c                                  \
-    $(NULL)
-
-libopenthread_cc2538_a_SOURCES            = \
-    $(PLATFORM_SOURCES)                     \
-    $(NULL)
-
-Dash                                      = -
-libopenthread_cc2538_a_LIBADD             = \
-    $(shell find $(top_builddir)/examples/platforms/utils $(Dash)type f $(Dash)name "*.o")
-
-include $(abs_top_nlbuild_autotools_dir)/automake/post.am
diff --git a/examples/platforms/cc2538/Makefile.platform.am b/examples/platforms/cc2538/Makefile.platform.am
deleted file mode 100644
index f908b4e..0000000
--- a/examples/platforms/cc2538/Makefile.platform.am
+++ /dev/null
@@ -1,39 +0,0 @@
-#
-#  Copyright (c) 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.
-#
-
-#
-# cc2538 platform-specific Makefile
-#
-
-LDADD_COMMON                                                          += \
-    $(top_builddir)/examples/platforms/cc2538/libopenthread-cc2538.a     \
-    $(NULL)
-
-LDFLAGS_COMMON                                                        += \
-    -T $(top_srcdir)/examples/platforms/cc2538/cc2538.ld                 \
-    $(NULL)
diff --git a/examples/platforms/cc2538/README.md b/examples/platforms/cc2538/README.md
index 91d5b7a..d6674dd 100644
--- a/examples/platforms/cc2538/README.md
+++ b/examples/platforms/cc2538/README.md
@@ -1,112 +1 @@
-# OpenThread on CC2538 Example
-
-This directory contains example platform drivers for the [Texas Instruments CC2538][cc2538].
-
-[cc2538]: http://www.ti.com/product/CC2538
-
-The example platform drivers are intended to present the minimal code necessary to support OpenThread. As a result, the example platform drivers do not necessarily highlight the platform's full capabilities.
-
-## Toolchain
-
-Download and install the [GNU toolchain for ARM Cortex-M][gnu-toolchain].
-
-[gnu-toolchain]: https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm
-
-In a Bash terminal, follow these instructions to install the GNU toolchain and other dependencies.
-
-```bash
-$ cd <path-to-openthread>
-$ ./script/bootstrap
-```
-
-## Building
-
-In a Bash terminal, follow these instructions to build the cc2538 examples.
-
-```bash
-$ cd <path-to-openthread>
-$ ./bootstrap
-$ make -f examples/Makefile-cc2538
-```
-
-### CC2592 support
-
-If your board has a CC2592 range extender front-end IC connected to the CC2538 (e.g. the CC2538-CC2592 EM reference design), you need to initialise this part before reception of radio traffic will work.
-
-Support is enabled in OpenThread by building with `CC2592=1`:
-
-```bash
-$ make -f examples/Makefile-cc2538 CC2592=1
-```
-
-The default settings should work for any design following the integration advice given in TI's application report ["AN130 - Using CC2592 Front End With CC2538"](http://www.ti.com/lit/pdf/swra447).
-
-Additional settings can be customised:
-
-- `CC2592_PA_EN`: This specifies which pin (on port C of the CC2538) connects to the CC2592's `PA_EN` pin. The default is `3` (PC3).
-- `CC2592_LNA_EN`: This specifies which pin (on port C of the CC2538) connects to the CC2592's `LNA_EN` pin. The default is `2` (PC2).
-- `CC2592_USE_HGM`: This defines whether the HGM pin of the CC2592 is under GPIO control or not. If not, it is assumed that the HGM pin is tied to a power rail.
-- `CC2592_HGM_PORT`: The HGM pin can be connected to any free GPIO. TI recommend using PD2, however if you've used a pin on another GPIO port, you may specify that port (`A`, `B` or `C`) here.
-- `CC2592_HGM_PORT`: The HGM pin can be connected to any free GPIO. TI recommend using PD2, however if you've used a pin on another GPIO port, you may specify that port (`A`, `B` or `C`) here. Default is `D`.
-- `CC2592_HGM_PIN`: The HGM pin can be connected to any free GPIO. TI recommend using PD2, however if you've used a pin on another GPIO pin, you can specify the pin here. Default is `2`.
-- `CC2592_HGM_DEFAULT_STATE`: By default, HGM is enabled at power-on, but you may want to have it default to off, specify `CC2592_HGM_DEFAULT_STATE=0` to do so.
-- `CC2538_RECEIVE_SENSITIVITY`: If you have tied the HGM pin to a power rail, this allows you to calibrate the RSSI values according to the new receive sensitivity. This has no effect if `CC2592_USE_HGM=1` (the default).
-- `CC2538_RSSI_OFFSET`: If you have tied the HGM pin to a power rail, this allows you to calibrate the RSSI values according to the new RSSI offset. This has no effect if `CC2592_USE_HGM=1` (the default).
-
-## Flash Binaries
-
-If the build completed successfully, the `elf` files may be found in `<path-to-openthread>/output/cc2538/bin`.
-
-To flash the images with [Flash Programmer 2][ti-flash-programmer-2], the files must have the `*.elf` extension.
-
-```bash
-$ cd <path-to-openthread>/output/cc2538/bin
-$ cp ot-cli ot-cli.elf
-```
-
-To load the images with the [serial bootloader][ti-cc2538-bootloader], the images must be converted to `bin`. This is done using `arm-none-eabi-objcopy`
-
-```bash
-$ cd <path-to-openthread>/output/cc2538/bin
-$ arm-none-eabi-objcopy -O binary ot-cli ot-cli.bin
-```
-
-The [cc2538-bsl.py script][cc2538-bsl-tool] provides a convenient method for flashing a CC2538 via the UART. To enter the bootloader backdoor for flashing, hold down SELECT for CC2538DK (corresponds to logic '0') while you press the Reset button.
-
-[ti-flash-programmer-2]: http://www.ti.com/tool/flash-programmer
-[ti-cc2538-bootloader]: http://www.ti.com/lit/an/swra466a/swra466a.pdf
-[cc2538-bsl-tool]: https://github.com/JelmerT/cc2538-bsl
-
-## Interact
-
-1. Open terminal to `/dev/ttyUSB1` (serial port settings: 115200 8-N-1).
-2. Type `help` for list of commands.
-
-```bash
-> help
-help
-channel
-childtimeout
-contextreusedelay
-extaddr
-extpanid
-ipaddr
-keysequence
-leaderweight
-mode
-netdata register
-networkidtimeout
-networkkey
-networkname
-panid
-ping
-prefix
-releaserouterid
-rloc16
-route
-routerupgradethreshold
-scan
-start
-state
-stop
-```
+The OpenThread on CC2538 example has moved to https://github.com/openthread/ot-cc2538
diff --git a/examples/platforms/cc2538/alarm.c b/examples/platforms/cc2538/alarm.c
deleted file mode 100644
index 7deefda..0000000
--- a/examples/platforms/cc2538/alarm.c
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 the OpenThread platform abstraction for the alarm.
- *
- */
-
-#include <stdbool.h>
-#include <stdint.h>
-
-#include <openthread/config.h>
-#include <openthread/platform/alarm-milli.h>
-#include <openthread/platform/diag.h>
-
-#include "platform-cc2538.h"
-
-enum
-{
-    kSystemClock = 32000000, ///< MHz
-    kTicksPerSec = 1000,     ///< Ticks per second
-};
-
-static uint32_t sCounter   = 0;
-static uint32_t sAlarmT0   = 0;
-static uint32_t sAlarmDt   = 0;
-static bool     sIsRunning = false;
-
-static uint8_t  sTimersIsRunning = 0;
-static uint32_t sTimersExpireAt[OT_CC2538_TIMERS_COUNT];
-
-extern void cc2538EnergyScanTimerHandler(void);
-
-void cc2538SetTimer(otCC2538Timer aTimer, uint32_t aDelay)
-{
-    sTimersIsRunning |= (1 << aTimer);
-    sTimersExpireAt[aTimer] = sCounter + aDelay;
-}
-
-void cc2538AlarmInit(void)
-{
-    HWREG(NVIC_ST_RELOAD) = kSystemClock / kTicksPerSec;
-    HWREG(NVIC_ST_CTRL)   = NVIC_ST_CTRL_CLK_SRC | NVIC_ST_CTRL_INTEN | NVIC_ST_CTRL_ENABLE;
-}
-
-uint32_t otPlatAlarmMilliGetNow(void)
-{
-    return sCounter;
-}
-
-void otPlatAlarmMilliStartAt(otInstance *aInstance, uint32_t t0, uint32_t dt)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    sAlarmT0   = t0;
-    sAlarmDt   = dt;
-    sIsRunning = true;
-}
-
-void otPlatAlarmMilliStop(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    sIsRunning = false;
-}
-
-void cc2538AlarmProcess(otInstance *aInstance)
-{
-    uint32_t expires;
-    bool     fire = false;
-
-    if (sTimersIsRunning)
-    {
-        if ((int32_t)(sTimersExpireAt[OT_CC2538_TIMER_ENERGY_SCAN] - sCounter) < 0)
-        {
-            sTimersIsRunning &= ~(1 << OT_CC2538_TIMER_ENERGY_SCAN);
-            cc2538EnergyScanTimerHandler();
-        }
-    }
-
-    if (sIsRunning)
-    {
-        expires = sAlarmT0 + sAlarmDt;
-
-        if (sAlarmT0 <= sCounter)
-        {
-            if (expires >= sAlarmT0 && expires <= sCounter)
-            {
-                fire = true;
-            }
-        }
-        else
-        {
-            if (expires >= sAlarmT0 || expires <= sCounter)
-            {
-                fire = true;
-            }
-        }
-
-        if (fire)
-        {
-            sIsRunning = false;
-
-#if OPENTHREAD_CONFIG_DIAG_ENABLE
-
-            if (otPlatDiagModeGet())
-            {
-                otPlatDiagAlarmFired(aInstance);
-            }
-            else
-#endif
-            {
-                otPlatAlarmMilliFired(aInstance);
-            }
-        }
-    }
-}
-
-void SysTick_Handler()
-{
-    sCounter++;
-}
diff --git a/examples/platforms/cc2538/arm-none-eabi.cmake b/examples/platforms/cc2538/arm-none-eabi.cmake
deleted file mode 100644
index 2e39ec1..0000000
--- a/examples/platforms/cc2538/arm-none-eabi.cmake
+++ /dev/null
@@ -1,42 +0,0 @@
-#
-#  Copyright (c) 2019, 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(CMAKE_SYSTEM_NAME              Generic)
-set(CMAKE_SYSTEM_PROCESSOR         ARM)
-
-set(CMAKE_C_COMPILER               arm-none-eabi-gcc)
-set(CMAKE_CXX_COMPILER             arm-none-eabi-g++)
-set(CMAKE_ASM_COMPILER             arm-none-eabi-as)
-set(CMAKE_RANLIB                   arm-none-eabi-ranlib)
-
-set(COMMON_C_FLAGS                 "-mthumb -fno-builtin -Wall -fdata-sections -ffunction-sections -mabi=aapcs -mcpu=cortex-m3 -mfloat-abi=soft")
-
-set(CMAKE_C_FLAGS_INIT             "${COMMON_C_FLAGS} -std=gnu99")
-set(CMAKE_CXX_FLAGS_INIT           "${COMMON_C_FLAGS} -fno-exceptions -fno-rtti")
-set(CMAKE_ASM_FLAGS_INIT           "${COMMON_C_FLAGS}")
-set(CMAKE_EXE_LINKER_FLAGS_INIT    "${COMMON_C_FLAGS} -specs=nano.specs -specs=nosys.specs -nostartfiles")
diff --git a/examples/platforms/cc2538/cc2538-reg.h b/examples/platforms/cc2538/cc2538-reg.h
deleted file mode 100644
index 54972fb..0000000
--- a/examples/platforms/cc2538/cc2538-reg.h
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 CC2538 register definitions.
- *
- */
-
-#ifndef CC2538_REG_H_
-#define CC2538_REG_H_
-
-#include <stdint.h>
-
-// clang-format off
-
-#define HWREG(x)                                (*((volatile uint32_t *)(x)))
-
-/*!
- * For registers that are arrays of 32-bit integers.
- *
- * @param       reg     Register address
- * @param       idx     Register array index
- */
-#define HWREG_ARR(reg, idx)                     HWREG((reg) + ((idx) << 2))
-
-#define NVIC_ST_CTRL                            0xE000E010  // SysTick Control and Status
-#define NVIC_ST_RELOAD                          0xE000E014  // SysTick Reload Value Register
-#define NVIC_EN0                                0xE000E100  // Interrupt 0-31 Set Enable
-
-#define NVIC_ST_CTRL_COUNT                      0x00010000  // Count Flag
-#define NVIC_ST_CTRL_CLK_SRC                    0x00000004  // Clock Source
-#define NVIC_ST_CTRL_INTEN                      0x00000002  // Interrupt Enable
-#define NVIC_ST_CTRL_ENABLE                     0x00000001  // Enable
-
-#define RFCORE_XREG_SRCMATCH_EN                 0x00000001  // SRCMATCH.SRC_MATCH_EN(1)
-#define RFCORE_XREG_SRCMATCH_AUTOPEND           0x00000002  // SRCMATCH.AUTOPEND(1)
-#define RFCORE_XREG_SRCMATCH_PEND_DATAREQ_ONLY  0x00000004  // SRCMATCH.PEND_DATAREQ_ONLY(1)
-
-#define RFCORE_XREG_SRCMATCH_ENABLE_STATUS_SIZE 3           // Num of register for source match enable status
-#define RFCORE_XREG_SRCMATCH_SHORT_ENTRIES      24          // 24 short address entries in maximum
-#define RFCORE_XREG_SRCMATCH_EXT_ENTRIES        12          // 12 extended address entries in maximum
-#define RFCORE_XREG_SRCMATCH_SHORT_ENTRY_OFFSET 4           // address offset for one short address entry
-#define RFCORE_XREG_SRCMATCH_EXT_ENTRY_OFFSET   8           // address offset for one extended address entry
-
-#define INT_UART0                               21          // UART0 Rx and Tx
-
-#define IEEE_EUI64                              0x00280028  // Address of IEEE EUI-64 address
-
-#define RFCORE_FFSM_SRCADDRESS_TABLE            0x40088400  // Source Address Table
-
-#define RFCORE_FFSM_SRCEXTPENDEN0               0x40088590  // Enable/Disable automatic pending per extended address
-#define RFCORE_FFSM_SRCSHORTPENDEN0             0x4008859C  // Enable/Disable automatic pending per short address
-#define RFCORE_FFSM_EXT_ADDR0                   0x400885A8  // Local address information
-#define RFCORE_FFSM_PAN_ID0                     0x400885C8  // Local address information
-#define RFCORE_FFSM_PAN_ID1                     0x400885CC  // Local address information
-#define RFCORE_FFSM_SHORT_ADDR0                 0x400885D0  // Local address information
-#define RFCORE_FFSM_SHORT_ADDR1                 0x400885D4  // Local address information
-#define RFCORE_XREG_FRMFILT0                    0x40088600  // The frame filtering function
-#define RFCORE_XREG_SRCMATCH                    0x40088608  // Source address matching and pending bits
-#define RFCORE_XREG_SRCSHORTEN0                 0x4008860C  // Short address matching
-#define RFCORE_XREG_SRCEXTEN0                   0x40088618  // Extended address matching
-
-#define RFCORE_XREG_FRMCTRL0                    0x40088624  // Frame handling
-#define RFCORE_XREG_FRMCTRL1                    0x40088628  // Frame handling
-#define RFCORE_XREG_RXENABLE                    0x4008862C  // RX enabling
-#define RFCORE_XREG_FREQCTRL                    0x4008863C  // Controls the RF frequency
-#define RFCORE_XREG_TXPOWER                     0x40088640  // Controls the output power
-#define RFCORE_XREG_FSMSTAT0                    0x40088648  // Radio finite state machine status
-#define RFCORE_XREG_FSMSTAT1                    0x4008864C  // Radio status register
-#define RFCORE_XREG_FIFOPCTRL                   0x40088650  // FIFOP threshold
-#define RFCORE_XREG_CCACTRL0                    0x40088658  // CCA threshold
-#define RFCORE_XREG_RSSI                        0x40088660  // RSSI status register
-#define RFCORE_XREG_RSSISTAT                    0x40088664  // RSSI valid status register
-#define RFCORE_XREG_AGCCTRL1                    0x400886C8  // AGC reference level
-#define RFCORE_XREG_RFC_OBS_CTRL                0x400887AC  // RF Core observable output
-#define RFCORE_XREG_TXFILTCFG                   0x400887E8  // TX filter configuration
-#define RFCORE_XREG_RFRND                       0x4008869C  // Random data
-#define RFCORE_SFR_RFDATA                       0x40088828  // The TX FIFO and RX FIFO
-#define RFCORE_SFR_RFERRF                       0x4008882C  // RF error interrupt flags
-#define RFCORE_SFR_RFIRQF0                      0x40088834  // RF interrupt flags
-#define RFCORE_SFR_RFST                         0x40088838  // RF CSMA-CA/strobe processor
-#define CCTEST_OBSSEL                           0x44010014  // CCTEST observable output route
-
-#define RFCORE_XREG_FRMFILT0_FRAME_FILTER_EN    0x00000001  // Enables frame filtering
-
-#define RFCORE_XREG_FRMCTRL0_AUTOACK            0x00000020
-#define RFCORE_XREG_FRMCTRL0_ENERGY_SCAN        0x00000010
-#define RFCORE_XREG_FRMCTRL0_AUTOCRC            0x00000040
-#define RFCORE_XREG_FRMCTRL0_INFINITY_RX        0x00000008
-
-#define RFCORE_XREG_FRMCTRL1_PENDING_OR         0x00000004
-
-#define RFCORE_XREG_RFRND_IRND                  0x00000001
-
-#define RFCORE_XREG_FSMSTAT0_STATE_MASK         0x0000003F
-#define RFCORE_XREG_FSMSTAT0_CAL_DONE           0x00000080
-#define RFCORE_XREG_FSMSTAT0_CAL_RUN            0x00000040
-
-#define RFCORE_XREG_FSMSTAT0_STATE_IDLE         0x00000000
-#define RFCORE_XREG_FSMSTAT0_STATE_RX_CAL       0x00000002
-#define RFCORE_XREG_FSMSTAT0_STATE_SFD_WAIT0    0x00000003
-#define RFCORE_XREG_FSMSTAT0_STATE_SFD_WAIT1    0x00000004
-#define RFCORE_XREG_FSMSTAT0_STATE_SFD_WAIT2    0x00000005
-#define RFCORE_XREG_FSMSTAT0_STATE_SFD_WAIT3    0x00000006
-#define RFCORE_XREG_FSMSTAT0_STATE_RX0          0x00000007
-#define RFCORE_XREG_FSMSTAT0_STATE_RX1          0x00000008
-#define RFCORE_XREG_FSMSTAT0_STATE_RX2          0x00000009
-#define RFCORE_XREG_FSMSTAT0_STATE_RX3          0x0000000A
-#define RFCORE_XREG_FSMSTAT0_STATE_RX4          0x0000000B
-#define RFCORE_XREG_FSMSTAT0_STATE_RX5          0x0000000C
-#define RFCORE_XREG_FSMSTAT0_STATE_RX6          0x0000000D
-#define RFCORE_XREG_FSMSTAT0_STATE_RX_WAIT      0x0000000E
-#define RFCORE_XREG_FSMSTAT0_STATE_RX_FRST      0x00000010
-#define RFCORE_XREG_FSMSTAT0_STATE_RX_OVER      0x00000011
-#define RFCORE_XREG_FSMSTAT0_STATE_TX_CAL       0x00000020
-#define RFCORE_XREG_FSMSTAT0_STATE_TX0          0x00000022
-#define RFCORE_XREG_FSMSTAT0_STATE_TX1          0x00000023
-#define RFCORE_XREG_FSMSTAT0_STATE_TX2          0x00000024
-#define RFCORE_XREG_FSMSTAT0_STATE_TX3          0x00000025
-#define RFCORE_XREG_FSMSTAT0_STATE_TX4          0x00000026
-#define RFCORE_XREG_FSMSTAT0_STATE_TX_FINAL     0x00000027
-#define RFCORE_XREG_FSMSTAT0_STATE_RXTX_TRANS   0x00000028
-#define RFCORE_XREG_FSMSTAT0_STATE_ACK_CAL      0x00000030
-#define RFCORE_XREG_FSMSTAT0_STATE_ACK0         0x00000031
-#define RFCORE_XREG_FSMSTAT0_STATE_ACK1         0x00000032
-#define RFCORE_XREG_FSMSTAT0_STATE_ACK2         0x00000033
-#define RFCORE_XREG_FSMSTAT0_STATE_ACK3         0x00000034
-#define RFCORE_XREG_FSMSTAT0_STATE_ACK4         0x00000035
-#define RFCORE_XREG_FSMSTAT0_STATE_ACK5         0x00000036
-#define RFCORE_XREG_FSMSTAT0_STATE_ACK_DELAY    0x00000037
-#define RFCORE_XREG_FSMSTAT0_STATE_TX_UNDER     0x00000038
-#define RFCORE_XREG_FSMSTAT0_STATE_TX_DOWN0     0x0000001A
-#define RFCORE_XREG_FSMSTAT0_STATE_TX_DOWN1     0x0000003A
-
-#define RFCORE_XREG_FSMSTAT1_RX_ACTIVE          0x00000001
-#define RFCORE_XREG_FSMSTAT1_TX_ACTIVE          0x00000002
-#define RFCORE_XREG_FSMSTAT1_LOCK_STATUS        0x00000004
-#define RFCORE_XREG_FSMSTAT1_SAMPLED_CCA        0x00000008
-#define RFCORE_XREG_FSMSTAT1_CCA                0x00000010  // Clear channel assessment
-#define RFCORE_XREG_FSMSTAT1_SFD                0x00000020
-#define RFCORE_XREG_FSMSTAT1_FIFOP              0x00000040
-#define RFCORE_XREG_FSMSTAT1_FIFO               0x00000080
-
-#define RFCORE_XREG_RSSISTAT_RSSI_VALID         0x00000001  // RSSI value is valid.
-
-#define RFCORE_XREG_RFC_OBS_POL_INV             0x00000040  // Invert polarity of OBS signal
-#define RFCORE_XREG_RFC_OBS_MUX_ZERO            0x00000000  // Observable = constant zero
-#define RFCORE_XREG_RFC_OBS_MUX_ONE             0x00000001  // Observable = constant one
-#define RFCORE_XREG_RFC_OBS_MUX_SNIFF_DATA      0x00000008  // RFC sniff data
-#define RFCORE_XREG_RFC_OBS_MUX_SNIFF_CLK       0x00000009  // RFC sniff clock
-#define RFCORE_XREG_RFC_OBS_MUX_RSSI_VALID      0x0000000c  // RSSI valid
-#define RFCORE_XREG_RFC_OBS_MUX_DEMOD_CCA       0x0000000d  // Clear channel assessment
-#define RFCORE_XREG_RFC_OBS_MUX_SAMPLED_CCA     0x0000000e  // Sampled CCA signal
-#define RFCORE_XREG_RFC_OBS_MUX_SFD_SYNC        0x0000000f  // SFD received or transmitted
-#define RFCORE_XREG_RFC_OBS_MUX_TX_ACTIVE       0x00000010  // Transmitter is active
-#define RFCORE_XREG_RFC_OBS_MUX_RX_ACTIVE       0x00000011  // Receiver is active
-#define RFCORE_XREG_RFC_OBS_MUX_FFCTRL_FIFO     0x00000012  // One or more bytes in FIFO
-#define RFCORE_XREG_RFC_OBS_MUX_FFCTRL_FIFOP    0x00000013  // One or more frames in FIFO
-#define RFCORE_XREG_RFC_OBS_MUX_PACKET_DONE     0x00000014  // Packet received
-#define RFCORE_XREG_RFC_OBS_MUX_RFC_XOR_RAND_IQ 0x00000016  // RAND I ^ RAND Q
-#define RFCORE_XREG_RFC_OBS_MUX_RFC_RAND_Q      0x00000017  // Random data from Q channel
-#define RFCORE_XREG_RFC_OBS_MUX_RFC_RAND_I      0x00000018  // Random data from I channel
-#define RFCORE_XREG_RFC_OBS_MUX_LOCK_STATUS     0x00000019  // PLL is in lock
-#define RFCORE_XREG_RFC_OBS_MUX_PA_PD           0x00000028  // Power amp power down
-#define RFCORE_XREG_RFC_OBS_MUX_LNA_PD          0x0000002a  // LNA power down
-
-#define RFCORE_SFR_RFERRF_NLOCK                 0x00000001  // Failed to achieve PLL lock.
-#define RFCORE_SFR_RFERRF_RXABO                 0x00000002  // RX Aborted.
-#define RFCORE_SFR_RFERRF_RXOVERF               0x00000004  // RX FIFO overflowed.
-#define RFCORE_SFR_RFERRF_RXUNDERF              0x00000008  // RX FIFO underflowed.
-#define RFCORE_SFR_RFERRF_TXOVERF               0x00000010  // TX FIFO overflowed.
-#define RFCORE_SFR_RFERRF_TXUNDERF              0x00000020  // TX FIFO underflowed.
-#define RFCORE_SFR_RFERRF_STROBEERR             0x00000040  // Command Strobe Error.
-
-#define RFCORE_SFR_RFST_INSTR_RXON              0xE3        // Instruction set RX on
-#define RFCORE_SFR_RFST_INSTR_TXON              0xE9        // Instruction set TX on
-#define RFCORE_SFR_RFST_INSTR_RFOFF             0xEF        // Instruction set RF off
-#define RFCORE_SFR_RFST_INSTR_FLUSHRX           0xED        // Instruction set flush rx buffer
-#define RFCORE_SFR_RFST_INSTR_FLUSHTX           0xEE        // Instruction set flush tx buffer
-
-#define CCTEST_OBSSEL_EN                        0x00000080  // Enable the OBS output on this pin
-#define CCTEST_OBSSEL_SEL_OBS0                  0x00000000  // Route OBS0 to pin
-#define CCTEST_OBSSEL_SEL_OBS1                  0x00000001  // Route OBS1 to pin
-#define CCTEST_OBSSEL_SEL_OBS2                  0x00000002  // Route OBS2 to pin
-
-#define ANA_REGS_BASE                           0x400D6000  // ANA_REGS
-#define ANA_REGS_O_IVCTRL                       0x00000004  // Analog control register
-
-#define SYS_CTRL_CLOCK_CTRL                     0x400D2000  // The clock control register
-#define SYS_CTRL_SYSDIV_32MHZ                   0x00000000  // Sys_div for sysclk 32MHz
-#define SYS_CTRL_CLOCK_CTRL_AMP_DET             0x00200000
-
-#define SYS_CTRL_PWRDBG                         0x400D2074
-#define SYS_CTRL_PWRDBG_FORCE_WARM_RESET        0x00000008
-
-#define SYS_CTRL_RCGCUART                       0x400D2028
-#define SYS_CTRL_SCGCUART                       0x400D202C
-#define SYS_CTRL_DCGCUART                       0x400D2030
-#define SYS_CTRL_I_MAP                          0x400D2098
-#define SYS_CTRL_RCGCRFC                        0x400D20A8
-#define SYS_CTRL_SCGCRFC                        0x400D20AC
-#define SYS_CTRL_DCGCRFC                        0x400D20B0
-#define SYS_CTRL_EMUOVR                         0x400D20B4
-
-#define SYS_CTRL_RCGCRFC_RFC0                   0x00000001
-#define SYS_CTRL_SCGCRFC_RFC0                   0x00000001
-#define SYS_CTRL_DCGCRFC_RFC0                   0x00000001
-
-#define SYS_CTRL_I_MAP_ALTMAP                   0x00000001
-
-#define SYS_CTRL_RCGCUART_UART0                 0x00000001
-#define SYS_CTRL_SCGCUART_UART0                 0x00000001
-#define SYS_CTRL_DCGCUART_UART0                 0x00000001
-
-#define SYS_CTRL_RCGCUART_UART1                 0x00000002
-#define SYS_CTRL_SCGCUART_UART1                 0x00000002
-#define SYS_CTRL_DCGCUART_UART1                 0x00000002
-
-#define IOC_PA0_SEL                             0x400D4000  // Peripheral select control
-#define IOC_PA1_SEL                             0x400D4004  // Peripheral select control
-#define IOC_PA2_SEL                             0x400D4008
-#define IOC_PA3_SEL                             0x400D400C
-#define IOC_UARTRXD_UART0                       0x400D4100
-#define IOC_UARTRXD_UART1                       0x400D4108
-
-#define IOC_PA0_OVER                            0x400D4080
-#define IOC_PA1_OVER                            0x400D4084
-#define IOC_PA2_OVER                            0x400D4088
-#define IOC_PA3_OVER                            0x400D408C
-
-#define IOC_MUX_OUT_SEL_UART0_TXD               0x00000000
-#define IOC_MUX_OUT_SEL_UART1_TXD               0x00000002
-
-#define IOC_OVERRIDE_OE                         0x00000008  // PAD Config Override Output Enable
-#define IOC_OVERRIDE_DIS                        0x00000000  // PAD Config Override Disabled
-
-#define IOC_PAD_IN_SEL_PA0                      0x00000000  // PA0
-#define IOC_PAD_IN_SEL_PA1                      0x00000001  // PA1
-#define IOC_PAD_IN_SEL_PA2                      0x00000002  // PA2
-#define IOC_PAD_IN_SEL_PA3                      0x00000003  // PA3
-
-#define UART0_BASE                              0x4000C000
-#define UART1_BASE                              0x4000D000
-#define GPIO_A_BASE                             0x400D9000  // GPIO A
-#define GPIO_B_BASE                             0x400DA000  // GPIO B
-#define GPIO_C_BASE                             0x400DB000  // GPIO C
-#define GPIO_D_BASE                             0x400DC000  // GPIO D
-
-#define GPIO_O_DIR                              0x00000400
-#define GPIO_O_AFSEL                            0x00000420
-
-#define GPIO_PIN(x)                             (1UL << x)  // Arbitrary GPIO pin
-#define GPIO_PIN_0                              0x00000001  // GPIO pin 0
-#define GPIO_PIN_1                              0x00000002  // GPIO pin 1
-#define GPIO_PIN_2                              0x00000004  // GPIO pin 2
-#define GPIO_PIN_3                              0x00000008  // GPIO pin 3
-#define GPIO_PIN_4                              0x00000010  // GPIO pin 4
-#define GPIO_PIN_5                              0x00000020  // GPIO pin 5
-#define GPIO_PIN_6                              0x00000040  // GPIO pin 6
-#define GPIO_PIN_7                              0x00000080  // GPIO pin 7
-
-#define UART_O_DR                               0x00000000  // UART data
-#define UART_O_FR                               0x00000018  // UART flag
-#define UART_O_IBRD                             0x00000024
-#define UART_O_FBRD                             0x00000028
-#define UART_O_LCRH                             0x0000002C
-#define UART_O_CTL                              0x00000030  // UART control
-#define UART_O_IM                               0x00000038  // UART interrupt mask
-#define UART_O_MIS                              0x00000040  // UART masked interrupt status
-#define UART_O_ICR                              0x00000044  // UART interrupt clear
-#define UART_O_CC                               0x00000FC8  // UART clock configuration
-
-#define UART_FR_RXFE                            0x00000010  // UART receive FIFO empty
-#define UART_FR_TXFF                            0x00000020  // UART transmit FIFO full
-#define UART_FR_RXFF                            0x00000040  // UART receive FIFO full
-
-#define UART_CONFIG_WLEN_8                      0x00000060  // 8 bit data
-#define UART_CONFIG_STOP_ONE                    0x00000000  // One stop bit
-#define UART_CONFIG_PAR_NONE                    0x00000000  // No parity
-
-#define UART_CTL_UARTEN                         0x00000001  // UART enable
-#define UART_CTL_TXE                            0x00000100  // UART transmit enable
-#define UART_CTL_RXE                            0x00000200  // UART receive enable
-
-#define UART_IM_RXIM                            0x00000010  // UART receive interrupt mask
-#define UART_IM_RTIM                            0x00000040  // UART receive time-out interrupt
-
-#define SOC_ADC_ADCCON1                         0x400D7000  // ADC Control
-#define SOC_ADC_RNDL                            0x400D7014  // RNG low data
-#define SOC_ADC_RNDH                            0x400D7018  // RNG high data
-
-#define SOC_ADC_ADCCON1_RCTRL0                  0x00000004  // ADCCON1 RCTRL bit 0
-#define SOC_ADC_ADCCON1_RCTRL1                  0x00000008  // ADCCON1 RCTRL bit 1
-
-#define FLASH_CTRL_FCTL                         0x400D3008  // Flash control
-#define FLASH_CTRL_DIECFG0                      0x400D3014  // Flash information
-
-// clang-format on
-
-#endif
diff --git a/examples/platforms/cc2538/cc2538.ld b/examples/platforms/cc2538/cc2538.ld
deleted file mode 100644
index daff162..0000000
--- a/examples/platforms/cc2538/cc2538.ld
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- *  Copyright (c) 2016, 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
- *   GCC linker script for CC2538.
- */
-
-_512k_bytes             = (512*1024);
-_256k_bytes             = (256*1024);
-_128k_bytes             = (128*1024);
-_FLASH_page_size        = 2048;
-
-/*
- * Change for your chip, default is 512k chips
- */
-_FLASH_size_bytes       = _512k_bytes;
-_FLASH_n_pages          = (_FLASH_size_bytes / _FLASH_page_size);
-/* reduce the usable size by: the CCA + settings Page A & B, total 3 pages */
-_FLASH_usable_size      = (_FLASH_size_bytes - (3 * _FLASH_page_size));
-_FLASH_start            = 0x00200000;
-_FLASH_end              = (_FLASH_start + _FLASH_size_bytes);
-
-/*
- * The CCA (Customer Configuration Area) is always the last page.
- * See: http://www.ti.com/lit/ug/swru319c/swru319c.pdf
- * table 8-2 for more details.
- */
-_FLASH_cca_page         = (_FLASH_end - (1 * _FLASH_page_size));
-
-/*
- * OpenThread NV storage goes in the settings page.
- * OpenThread requires at least 2 adjacent pages, call them A and B.
- */
-_FLASH_settings_pageB   = (_FLASH_end - (2 * _FLASH_page_size));
-_FLASH_settings_pageA   = (_FLASH_end - (3 * _FLASH_page_size));
-
-MEMORY
-{
-  /* would like to use SYMBOLS (from above)here but we cannot
-   * GCC version 4.9 does not support symbolic expressions here.
-   * But later versions do support the feature.
-   */
-  FLASH (rx) :     ORIGIN = 0x00200000,     LENGTH = 0x0007c000
-  FLASH_CCA (rx) : ORIGIN = 0x0027FFD4,     LENGTH = 0x2c
-  SRAM (rwx) :     ORIGIN = 0x20000000,     LENGTH = 32K
-}
-/*
- * To safty check what would have been the SYMBOL values
- * we use these ASSERTS to verify things are still good.
- */
-ASSERT( _FLASH_start       == 0x00200000, "invalid flash start address for cc2538")
-ASSERT( _FLASH_cca_page    == 0x0027f800, "invalid cca start address for cc2538")
-ASSERT( _FLASH_usable_size == 0x0007e800, "Invalid usable size for this config")
-
-
-
-ENTRY(flash_cca_lock_page)
-SECTIONS
-{
-    .text : ALIGN(4)
-    {
-        _text = .;
-        *(.vectors)
-        *(.text*)
-        *(.rodata*)
-        KEEP(*(.init))
-        KEEP(*(.fini))
-        _etext = .;
-    } > FLASH= 0
-
-    .init_array :
-    {
-        _init_array = .;
-        KEEP(*(SORT(.init_array.*)))
-        KEEP(*(.init_array*))
-        _einit_array = .;
-    } > FLASH
-
-    .ARM.exidx : ALIGN(4)
-    {
-        *(.ARM.exidx*)
-    } > FLASH
-
-    .data : ALIGN(4)
-    {
-        _data = .;
-        *(.data*)
-        _edata = .;
-    } > SRAM AT > FLASH
-    _ldata = LOADADDR(.data);
-
-    .bss : ALIGN(4)
-    {
-        _bss = .;
-        *(.bss*)
-        *(COMMON)
-        _ebss = .;
-    } > SRAM
-
-    _heap = .;
-    end = .;
-
-    .stack : ALIGN(4)
-    {
-        *(.stack)
-    } > SRAM
-
-    .flashcca :
-    {
-        KEEP(*(.flash_cca))
-    } > FLASH_CCA
-}
diff --git a/examples/platforms/cc2538/diag.c b/examples/platforms/cc2538/diag.c
deleted file mode 100644
index 579c1e7..0000000
--- a/examples/platforms/cc2538/diag.c
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 <stdbool.h>
-#include <stdio.h>
-#include <string.h>
-#include <sys/time.h>
-
-#include <openthread/config.h>
-#include <openthread/platform/alarm-milli.h>
-#include <openthread/platform/radio.h>
-
-#include "platform-cc2538.h"
-
-#if OPENTHREAD_CONFIG_DIAG_ENABLE
-
-/**
- * Diagnostics mode variables.
- *
- */
-static bool sDiagMode = false;
-
-void otPlatDiagModeSet(bool aMode)
-{
-    sDiagMode = aMode;
-}
-
-bool otPlatDiagModeGet()
-{
-    return sDiagMode;
-}
-
-void otPlatDiagChannelSet(uint8_t aChannel)
-{
-    OT_UNUSED_VARIABLE(aChannel);
-}
-
-void otPlatDiagTxPowerSet(int8_t aTxPower)
-{
-    OT_UNUSED_VARIABLE(aTxPower);
-}
-
-void otPlatDiagRadioReceived(otInstance *aInstance, otRadioFrame *aFrame, otError aError)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-    OT_UNUSED_VARIABLE(aFrame);
-    OT_UNUSED_VARIABLE(aError);
-}
-
-void otPlatDiagAlarmCallback(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
-
-#endif // OPENTHREAD_CONFIG_DIAG_ENABLE
diff --git a/examples/platforms/cc2538/entropy.c b/examples/platforms/cc2538/entropy.c
deleted file mode 100644
index 47a31be..0000000
--- a/examples/platforms/cc2538/entropy.c
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- *  Copyright (c) 2019, 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 an entropy source based on ADC.
- *
- */
-
-#include <openthread/platform/entropy.h>
-
-#include <openthread/platform/radio.h>
-
-#include "platform-cc2538.h"
-#include "utils/code_utils.h"
-
-static void generateRandom(uint8_t *aOutput, uint16_t aOutputLength)
-{
-    uint32_t frmctrl0;
-
-    HWREG(SOC_ADC_ADCCON1) &= ~(SOC_ADC_ADCCON1_RCTRL1 | SOC_ADC_ADCCON1_RCTRL0);
-    HWREG(SYS_CTRL_RCGCRFC) = SYS_CTRL_RCGCRFC_RFC0;
-
-    while (HWREG(SYS_CTRL_RCGCRFC) != SYS_CTRL_RCGCRFC_RFC0)
-        ;
-
-    frmctrl0                    = HWREG(RFCORE_XREG_FRMCTRL0);
-    HWREG(RFCORE_XREG_FRMCTRL0) = RFCORE_XREG_FRMCTRL0_INFINITY_RX;
-    HWREG(RFCORE_SFR_RFST)      = RFCORE_SFR_RFST_INSTR_RXON;
-
-    while (!HWREG(RFCORE_XREG_RSSISTAT) & RFCORE_XREG_RSSISTAT_RSSI_VALID)
-        ;
-
-    for (uint16_t index = 0; index < aOutputLength; index++)
-    {
-        aOutput[index] = 0;
-
-        for (uint8_t offset = 0; offset < 8 * sizeof(uint8_t); offset++)
-        {
-            aOutput[index] <<= 1;
-            aOutput[index] |= (HWREG(RFCORE_XREG_RFRND) & RFCORE_XREG_RFRND_IRND);
-        }
-    }
-
-    HWREG(RFCORE_SFR_RFST)      = RFCORE_SFR_RFST_INSTR_RFOFF;
-    HWREG(RFCORE_XREG_FRMCTRL0) = frmctrl0;
-}
-
-void cc2538RandomInit(void)
-{
-    uint16_t seed = 0;
-
-    while (seed == 0x0000 || seed == 0x8003)
-    {
-        generateRandom((uint8_t *)&seed, sizeof(seed));
-    }
-
-    HWREG(SOC_ADC_RNDL) = (seed >> 8) & 0xff;
-    HWREG(SOC_ADC_RNDL) = seed & 0xff;
-}
-
-otError otPlatEntropyGet(uint8_t *aOutput, uint16_t aOutputLength)
-{
-    otError error   = OT_ERROR_NONE;
-    uint8_t channel = 0;
-
-    otEXPECT_ACTION(aOutput, error = OT_ERROR_INVALID_ARGS);
-
-    if (sInstance && otPlatRadioIsEnabled(sInstance))
-    {
-        channel = 11 + (HWREG(RFCORE_XREG_FREQCTRL) - 11) / 5;
-        otPlatRadioSleep(sInstance);
-        otPlatRadioDisable(sInstance);
-    }
-
-    generateRandom(aOutput, aOutputLength);
-
-    if (channel)
-    {
-        cc2538RadioInit();
-        otPlatRadioEnable(sInstance);
-        otPlatRadioReceive(sInstance, channel);
-    }
-
-exit:
-    return error;
-}
diff --git a/examples/platforms/cc2538/flash.c b/examples/platforms/cc2538/flash.c
deleted file mode 100644
index 04c8a65..0000000
--- a/examples/platforms/cc2538/flash.c
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 <assert.h>
-#include <stdint.h>
-#include <string.h>
-
-#include "platform-cc2538.h"
-#include "rom-utility.h"
-
-#define FLASH_CTRL_FCTL_BUSY 0x00000080
-
-#define FLASH_PAGE_SIZE 2048
-#define FLASH_PAGE_NUM 2
-#define FLASH_SWAP_SIZE (FLASH_PAGE_SIZE * (FLASH_PAGE_NUM / 2))
-
-/* The linker script creates this external symbol */
-extern uint8_t _FLASH_settings_pageA[];
-
-/* Convert a settings offset to the physical address within the flash settings pages */
-static uint32_t flashPhysAddr(uint8_t aSwapIndex, uint32_t aOffset)
-{
-    uint32_t address = (uint32_t)(&_FLASH_settings_pageA[0]) + aOffset;
-
-    if (aSwapIndex)
-    {
-        address += FLASH_SWAP_SIZE;
-    }
-
-    return address;
-}
-
-void otPlatFlashInit(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
-
-uint32_t otPlatFlashGetSwapSize(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    return FLASH_SWAP_SIZE;
-}
-
-void otPlatFlashErase(otInstance *aInstance, uint8_t aSwapIndex)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    ROM_PageErase(flashPhysAddr(aSwapIndex, 0), FLASH_PAGE_SIZE);
-    while (HWREG(FLASH_CTRL_FCTL) & FLASH_CTRL_FCTL_BUSY)
-    {
-    }
-}
-
-void otPlatFlashWrite(otInstance *aInstance, uint8_t aSwapIndex, uint32_t aOffset, const void *aData, uint32_t aSize)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    uint32_t *data = (uint32_t *)(aData);
-
-    for (uint32_t size = 0; size < aSize; size += sizeof(uint32_t), aOffset += sizeof(uint32_t), data++)
-    {
-        ROM_ProgramFlash(data, flashPhysAddr(aSwapIndex, aOffset), sizeof(uint32_t));
-
-        while (HWREG(FLASH_CTRL_FCTL) & FLASH_CTRL_FCTL_BUSY)
-        {
-        }
-    }
-}
-
-void otPlatFlashRead(otInstance *aInstance, uint8_t aSwapIndex, uint32_t aOffset, uint8_t *aData, uint32_t aSize)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    memcpy(aData, (void *)flashPhysAddr(aSwapIndex, aOffset), aSize);
-}
diff --git a/examples/platforms/cc2538/logging.c b/examples/platforms/cc2538/logging.c
deleted file mode 100644
index 82158ae..0000000
--- a/examples/platforms/cc2538/logging.c
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 logging.c
- * Platform abstraction for the logging
- *
- */
-
-#include <openthread-core-config.h>
-#include <openthread/config.h>
-#include <openthread/platform/logging.h>
-#include <openthread/platform/toolchain.h>
-
-#if (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED)
-OT_TOOL_WEAK void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, ...)
-{
-    OT_UNUSED_VARIABLE(aLogLevel);
-    OT_UNUSED_VARIABLE(aLogRegion);
-    OT_UNUSED_VARIABLE(aFormat);
-}
-#endif
diff --git a/examples/platforms/cc2538/openthread-core-cc2538-config.h b/examples/platforms/cc2538/openthread-core-cc2538-config.h
deleted file mode 100644
index 1f26217..0000000
--- a/examples/platforms/cc2538/openthread-core-cc2538-config.h
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 cc2538 compile-time configuration constants for OpenThread.
- */
-
-#ifndef OPENTHREAD_CORE_CC2538_CONFIG_H_
-#define OPENTHREAD_CORE_CC2538_CONFIG_H_
-
-/**
- * @def OPENTHREAD_CONFIG_PLATFORM_INFO
- *
- * The platform-specific string to insert into the OpenThread version string.
- *
- */
-#define OPENTHREAD_CONFIG_PLATFORM_INFO "CC2538"
-
-/**
- * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_ACK_TIMEOUT_ENABLE
- *
- * Define to 1 if you want to enable software ACK timeout logic.
- *
- */
-#define OPENTHREAD_CONFIG_MAC_SOFTWARE_ACK_TIMEOUT_ENABLE 1
-
-/**
- * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_RETRANSMIT_ENABLE
- *
- * Define to 1 if you want to enable software retransmission logic.
- *
- */
-#define OPENTHREAD_CONFIG_MAC_SOFTWARE_RETRANSMIT_ENABLE 1
-
-/**
- * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_CSMA_BACKOFF_ENABLE
- *
- * Define to 1 if you want to enable software CSMA-CA backoff logic.
- *
- */
-#define OPENTHREAD_CONFIG_MAC_SOFTWARE_CSMA_BACKOFF_ENABLE 1
-
-/**
- * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE
- *
- * Define to 1 if you want to enable software transmission security logic.
- *
- */
-#define OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE 0
-
-/**
- * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_ENERGY_SCAN_ENABLE
- *
- * Define to 1 if you want to enable software energy scanning logic.
- *
- */
-#define OPENTHREAD_CONFIG_MAC_SOFTWARE_ENERGY_SCAN_ENABLE 1
-
-/**
- * @def OPENTHREAD_CONFIG_NCP_HDLC_ENABLE
- *
- * Define to 1 to enable NCP HDLC support.
- *
- */
-#define OPENTHREAD_CONFIG_NCP_HDLC_ENABLE 1
-
-/**
- * @def OPENTHREAD_CONFIG_CC2538_USE_RADIO_RX_INTERRUPT
- *
- * Enable support for using interrupt-driven radio reception.  This allows
- * for a single frame to be received whilst the CPU is busy processing some
- * other code.
- *
- * To disable interrupts and just rely on polling, set this to 0.
- *
- */
-#ifndef OPENTHREAD_CONFIG_CC2538_USE_RADIO_RX_INTERRUPT
-#define OPENTHREAD_CONFIG_CC2538_USE_RADIO_RX_INTERRUPT 1
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_CC2538_WITH_CC2592
- *
- * Enable support for the CC2592 range-extender front-end.
- *
- * This is a feature of the CC2538-CC2592 EM and other peripherals which
- * extends the range of the bare CC2538 to over a kilometre line-of-sight.
- * The CC2592 needs to be wired up to the RF port on the CC2538 in accordance
- * with application note 130 ("Using CC2592 Front End With CC2538", TI doc
- * SWRA447).
- *
- * If you have such a board, change this to 1.
- *
- */
-#ifndef OPENTHREAD_CONFIG_CC2538_WITH_CC2592
-#define OPENTHREAD_CONFIG_CC2538_WITH_CC2592 0
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_CC2592_PA_EN_PIN
- *
- * Define the pin (on port C) that connects to the CC2592 PA_EN pin.
- *
- * One of the 3 observable channels on the CC2538 radio module will be
- * configured to take the "PA power down" signal from the radio module itself,
- * invert it, and emit it on this GPIO pin.  Due to hardware constraints, it
- * may only be connected to a pin on GPIO port C.
- *
- * The default (PC3) is as per TI recommendations in AN130.
- *
- */
-#ifndef OPENTHREAD_CONFIG_CC2592_PA_EN_PIN
-#define OPENTHREAD_CONFIG_CC2592_PA_EN_PIN 3
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_CC2592_LNA_EN_PIN
- *
- * Define the pin (on port C) that connects to the CC2592 LNA_EN pin.
- *
- * One of the 3 observable channels on the CC2538 radio module will be
- * configured to take the "LNA power down" signal from the radio module itself,
- * invert it, and emit it on this GPIO pin.  Due to hardware constraints, it
- * may only be connected to a pin on GPIO port C.
- *
- * The default (PC2) is as per TI recommendations in AN130.
- *
- */
-#ifndef OPENTHREAD_CONFIG_CC2592_LNA_EN_PIN
-#define OPENTHREAD_CONFIG_CC2592_LNA_EN_PIN 2
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_CC2592_USE_HGM
- *
- * Enable control of the high-gain mode signal.
- *
- * High-gain mode is enabled through the `HGM` pin on the CC2592, which may be
- * connected to any free GPIO pin for software control, or may be linked to
- * VDD or 0V to hard-wire it to a given state.
- *
- * Set this to 0 if you have wired this pin to a power rail, or have a
- * non-standard way of controlling it.
- *
- */
-#ifndef OPENTHREAD_CONFIG_CC2592_USE_HGM
-#define OPENTHREAD_CONFIG_CC2592_USE_HGM 1
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_CC2538_RECEIVE_SENSITIVITY
- *
- * Set the CC2538 receive sensitivity.
- *
- * A bare CC2538 has a receive sensitivity of -88dBm.  The CC2592 changes this
- * to -85 or -81 depending on whether the HGM pin is high or low.  If
- * `OPENTHREAD_CONFIG_CC2592_USE_HGM` is 0, then this sets the receive
- * sensitivity.
- *
- */
-#ifndef OPENTHREAD_CONFIG_CC2538_RECEIVE_SENSITIVITY
-#define OPENTHREAD_CONFIG_CC2538_RECEIVE_SENSITIVITY -88
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_CC2538_RSSI_OFFSET
- *
- * Set the CC2538 RSSI offset.  This calibrates the RSSI readings received from
- * the CC2538 radio module to give a reading in dBm.
- *
- * For a standard CC2538 (no front-end), the RSSI offset is 73.
- *
- * For a CC2592 hard-wired in high-gain mode, an offset of 85 should be used;
- * or for low-gain mode, 81.  If `OPENTHREAD_CONFIG_CC2592_USE_HGM` is 0, then
- * this calibrates the RSSI value accordingly.
- */
-#ifndef OPENTHREAD_CONFIG_CC2538_RSSI_OFFSET
-#define OPENTHREAD_CONFIG_CC2538_RSSI_OFFSET 73
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_CC2592_HGM_PORT
- *
- * Define the GPIO port that the HGM pin is connected to.  It may be
- * connected to any available GPIO pin.
- *
- * The default (GPIO port D) is as per TI recommendations.
- *
- */
-#ifndef OPENTHREAD_CONFIG_CC2592_HGM_PORT
-#define OPENTHREAD_CONFIG_CC2592_HGM_PORT GPIO_D_BASE
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_CC2592_HGM_PIN
- *
- * Define the pin on the GPIO port that the HGM pin is connected to.  It
- * may be connected to any available GPIO pin.
- *
- * The default (PD2) is as per TI recommendations.
- *
- */
-#ifndef OPENTHREAD_CONFIG_CC2592_HGM_PIN
-#define OPENTHREAD_CONFIG_CC2592_HGM_PIN 2
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_CC2592_HGM_DEFAULT_STATE
- *
- * Define the default state of the CC2592's HGM pin.
- *
- * The default is to turn high-gain mode on.
- *
- */
-#ifndef OPENTHREAD_CONFIG_CC2592_HGM_DEFAULT_STATE
-#define OPENTHREAD_CONFIG_CC2592_HGM_DEFAULT_STATE true
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_PLATFORM_FLASH_API_ENABLE
- *
- * Define to 1 to enable otPlatFlash* APIs to support non-volatile storage.
- *
- * When defined to 1, the platform MUST implement the otPlatFlash* APIs instead of the otPlatSettings* APIs.
- *
- */
-#define OPENTHREAD_CONFIG_PLATFORM_FLASH_API_ENABLE 1
-
-#endif // OPENTHREAD_CORE_CC2538_CONFIG_H_
diff --git a/examples/platforms/cc2538/platform-cc2538.h b/examples/platforms/cc2538/platform-cc2538.h
deleted file mode 100644
index f89b01b..0000000
--- a/examples/platforms/cc2538/platform-cc2538.h
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 the platform-specific initializers.
- *
- */
-
-#ifndef PLATFORM_CC2538_H_
-#define PLATFORM_CC2538_H_
-
-#include <openthread-core-config.h>
-#include <stdint.h>
-#include <openthread/config.h>
-#include <openthread/instance.h>
-
-#include "cc2538-reg.h"
-
-// Global OpenThread instance structure
-extern otInstance *sInstance;
-
-/**
- * Initialize the debug uart
- */
-void cc2538DebugUartInit(void);
-
-/**
- * This function initializes the alarm service used by OpenThread.
- *
- */
-void cc2538AlarmInit(void);
-
-/**
- * This function performs alarm driver processing.
- *
- * @param[in]  aInstance  The OpenThread instance structure.
- *
- */
-void cc2538AlarmProcess(otInstance *aInstance);
-
-/**
- * This function initializes the radio service used by OpenThread.
- *
- */
-void cc2538RadioInit(void);
-
-/**
- * This function performs radio driver processing.
- *
- * @param[in]  aInstance  The OpenThread instance structure.
- *
- */
-void cc2538RadioProcess(otInstance *aInstance);
-
-/**
- * This function initializes the random number service used by OpenThread.
- *
- */
-void cc2538RandomInit(void);
-
-/**
- * This function performs UART driver processing.
- *
- */
-void cc2538UartProcess(void);
-
-#if OPENTHREAD_CONFIG_CC2538_WITH_CC2592 && OPENTHREAD_CONFIG_CC2592_USE_HGM
-/**
- * Change the state of the CC2592 HGM pin.
- *
- * @param	aState	Whether or not to enable HGM
- */
-void cc2538RadioSetHgm(bool aState);
-
-/**
- * Retrieve the state of the CC2592 HGM pin.
- */
-bool cc2538RadioGetHgm(void);
-#endif // OPENTHREAD_CONFIG_CC2538_WITH_CC2592 && OPENTHREAD_CONFIG_CC2592_USE_HGM
-
-typedef enum
-{
-    OT_CC2538_TIMER_ENERGY_SCAN, ///< Internal timer for energy scan
-    OT_CC2538_TIMERS_COUNT,      ///< Number of internal timers
-} otCC2538Timer;
-
-/**
- * This function sets the internal timer.
- *
- * @param[in]   aTimer  The timer identifier.
- * @param[in]   aDelay  The delay to trigger the timer, and must be no more than `INT32_MAX`.
- *
- */
-void cc2538SetTimer(otCC2538Timer aTimer, uint32_t aDelay);
-
-#endif // PLATFORM_CC2538_H_
diff --git a/examples/platforms/cc2538/radio.c b/examples/platforms/cc2538/radio.c
deleted file mode 100644
index 0878f11..0000000
--- a/examples/platforms/cc2538/radio.c
+++ /dev/null
@@ -1,1273 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 the OpenThread platform abstraction for radio communication.
- *
- */
-
-#include <openthread/config.h>
-#include <openthread/logging.h>
-#include <openthread/platform/alarm-milli.h>
-#include <openthread/platform/diag.h>
-#include <openthread/platform/radio.h>
-
-#include "platform-cc2538.h"
-#include "utils/code_utils.h"
-
-#define RFCORE_XREG_RFIRQM0 0x4008868C // RF interrupt masks
-#define RFCORE_XREG_RFIRQM1 0x40088690 // RF interrupt masks
-#define RFCORE_XREG_RFERRM 0x40088694  // RF error interrupt mask
-
-#define RFCORE_SFR_RFIRQF0_RXMASKZERO 0x00000080      // RXENABLE is now completely clear
-#define RFCORE_SFR_RFIRQF0_RXPKTDONE 0x00000040       // A complete frame has been received
-#define RFCORE_SFR_RFIRQF0_FRAME_ACCEPTED 0x00000020  // Frame has passed frame filtering
-#define RFCORE_SFR_RFIRQF0_SRC_MATCH_FOUND 0x00000010 // Source match is found
-#define RFCORE_SFR_RFIRQF0_SRC_MATCH_DONE 0x00000008  // Source matching is complete
-#define RFCORE_SFR_RFIRQF0_FIFOP 0x00000004           // The number of bytes in the RX fifo is above threshold
-#define RFCORE_SFR_RFIRQF0_SFD 0x00000002             // SFD has been received or transmitted
-#define RFCORE_SFR_RFIRQF0_ACT_UNUSED 0x00000001      // Reserved
-
-#define RFCORE_XREG_RFIRQM0_RXMASKZERO 0x00000080
-#define RFCORE_XREG_RFIRQM0_RXPKTDONE 0x00000040
-#define RFCORE_XREG_RFIRQM0_FRAME_ACCEPTED 0x00000020
-#define RFCORE_XREG_RFIRQM0_SRC_MATCH_FOUND 0x00000010
-#define RFCORE_XREG_RFIRQM0_SRC_MATCH_DONE 0x00000008
-#define RFCORE_XREG_RFIRQM0_FIFOP 0x00000004
-#define RFCORE_XREG_RFIRQM0_SFD 0x00000002
-#define RFCORE_XREG_RFIRQM0_ACT_UNUSED 0x00000001
-
-#define RFCORE_SFR_RFIRQF1_CSP_WAIT 0x00000020
-#define RFCORE_SFR_RFIRQF1_CSP_STOP 0x00000010
-#define RFCORE_SFR_RFIRQF1_CSP_MANINT 0x00000008
-#define RFCORE_SFR_RFIRQF1_RF_IDLE 0x00000004
-#define RFCORE_SFR_RFIRQF1_TXDONE 0x00000002
-#define RFCORE_SFR_RFIRQF1_TXACKDONE 0x00000001
-
-#define RFCORE_XREG_RFIRQM1_CSP_WAIT 0x00000020
-#define RFCORE_XREG_RFIRQM1_CSP_STOP 0x00000010
-#define RFCORE_XREG_RFIRQM1_CSP_MANINT 0x00000008
-#define RFCORE_XREG_RFIRQM1_RF_IDLE 0x00000004
-#define RFCORE_XREG_RFIRQM1_TXDONE 0x00000002
-#define RFCORE_XREG_RFIRQM1_TXACKDONE 0x00000001
-
-#define RFCORE_XREG_RFERRM_STROBE_ERR 0x00000040
-#define RFCORE_XREG_RFERRM_TXUNDERF 0x00000020
-#define RFCORE_XREG_RFERRM_TXOVERF 0x00000010
-#define RFCORE_XREG_RFERRM_RXUNDERF 0x00000008
-#define RFCORE_XREG_RFERRM_RXOVERF 0x00000004
-#define RFCORE_XREG_RFERRM_RXABO 0x00000002
-#define RFCORE_XREG_RFERRM_NLOCK 0x00000001
-
-enum
-{
-    IEEE802154_MIN_LENGTH      = 5,
-    IEEE802154_MAX_LENGTH      = 127,
-    IEEE802154_ACK_LENGTH      = 5,
-    IEEE802154_FRAME_TYPE_MASK = 0x7,
-    IEEE802154_FRAME_TYPE_ACK  = 0x2,
-    IEEE802154_FRAME_PENDING   = 1 << 4,
-    IEEE802154_ACK_REQUEST     = 1 << 5,
-    IEEE802154_DSN_OFFSET      = 2,
-};
-
-enum
-{
-    CC2538_RSSI_OFFSET = OPENTHREAD_CONFIG_CC2538_RSSI_OFFSET,
-    // TI AN130 (SWRA447) Table 4 (bottom of page 3)
-    CC2592_RSSI_OFFSET_HGM = 85,
-    CC2592_RSSI_OFFSET_LGM = 81,
-    CC2538_CRC_BIT_MASK    = 0x80,
-    CC2538_LQI_BIT_MASK    = 0x7f,
-};
-
-// All values in dBm
-enum
-{
-    CC2538_RECEIVE_SENSITIVITY = OPENTHREAD_CONFIG_CC2538_RECEIVE_SENSITIVITY,
-    // TI AN130 (SWRA447) Table 3 (middle of page 3)
-    CC2592_RECEIVE_SENSITIVITY_LGM = -99,
-    CC2592_RECEIVE_SENSITIVITY_HGM = -101,
-};
-
-typedef struct TxPowerTable
-{
-    int8_t  mTxPowerVal;
-    uint8_t mTxPowerReg;
-} TxPowerTable;
-
-// The transmit power table.
-static const TxPowerTable sTxPowerTable[] = {
-#if OPENTHREAD_CONFIG_CC2538_WITH_CC2592
-    // CC2538 using CC2592 PA
-    // Values are from AN130 table 6 (page 4)
-    {22, 0xFF}, // 22.0dBm =~ 158.5mW
-    {21, 0xD5}, // 20.9dBm =~ 123.0mW
-    {20, 0xC5}, // 20.1dBm =~ 102.3mW
-    {19, 0xB0}, // 19.0dBm =~  79.4mW
-    {18, 0xA1}, // 17.8dBm =~  60.3mW
-    {16, 0x91}, // 16.4dBm =~  43.7mW
-    {15, 0x88}, // 14.9dBm =~  30.9mW
-    {13, 0x72}, // 13.0dBm =~  20.0mW
-    {11, 0x62}, // 11.0dBm =~  12.6mW
-    {10, 0x58}, //  9.5dBm =~   8.9mW
-    {8, 0x42},  //  7.5dBm =~   5.6mW
-#else
-    // CC2538 operating "bare foot"
-    // Values are from SmartRF Studio 2.4.0
-    {7, 0xFF},   //
-    {5, 0xED},   //
-    {3, 0xD5},   //
-    {1, 0xC5},   //
-    {0, 0xB6},   //
-    {-1, 0xB0},  //
-    {-3, 0xA1},  //
-    {-5, 0x91},  //
-    {-7, 0x88},  //
-    {-9, 0x72},  //
-    {-11, 0x62}, //
-    {-13, 0x58}, //
-    {-15, 0x42}, //
-    {-24, 0x00}, //
-#endif
-};
-
-static otRadioFrame sTransmitFrame;
-static otRadioFrame sReceiveFrame;
-static otError      sTransmitError;
-static otError      sReceiveError;
-
-static uint8_t sTransmitPsdu[IEEE802154_MAX_LENGTH];
-static uint8_t sReceivePsdu[IEEE802154_MAX_LENGTH];
-static uint8_t sChannel = 0;
-static int8_t  sTxPower = 0;
-
-static otRadioState sState             = OT_RADIO_STATE_DISABLED;
-static bool         sIsReceiverEnabled = false;
-
-#if OPENTHREAD_CONFIG_LOG_PLATFORM && OPENTHREAD_CONFIG_CC2538_USE_RADIO_RX_INTERRUPT
-// Debugging _and_ logging are enabled, so if there's a dropped frame
-// we'll need to store the length here as using snprintf from an interrupt
-// handler is not a good idea.
-static uint8_t sDroppedFrameLength = 0;
-#endif
-
-static int8_t cc2538RadioGetRssiOffset(void);
-
-void enableReceiver(void)
-{
-    if (!sIsReceiverEnabled)
-    {
-        otLogInfoPlat("Enabling receiver", NULL);
-
-        // flush rxfifo
-        HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHRX;
-        HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHRX;
-
-        // enable receiver
-        HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_RXON;
-        sIsReceiverEnabled     = true;
-    }
-}
-
-void disableReceiver(void)
-{
-    if (sIsReceiverEnabled)
-    {
-        otLogInfoPlat("Disabling receiver", NULL);
-
-        while (HWREG(RFCORE_XREG_FSMSTAT1) & RFCORE_XREG_FSMSTAT1_TX_ACTIVE)
-            ;
-
-        // flush rxfifo
-        HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHRX;
-        HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHRX;
-
-        if (HWREG(RFCORE_XREG_RXENABLE) != 0)
-        {
-            // disable receiver
-            HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_RFOFF;
-        }
-
-        sIsReceiverEnabled = false;
-    }
-}
-
-void setChannel(uint8_t aChannel)
-{
-    if (sChannel != aChannel)
-    {
-        bool enabled = false;
-
-        if (sIsReceiverEnabled)
-        {
-            disableReceiver();
-            enabled = true;
-        }
-
-        otLogInfoPlat("Channel=%d", aChannel);
-
-        HWREG(RFCORE_XREG_FREQCTRL) = 11 + (aChannel - 11) * 5;
-        sChannel                    = aChannel;
-
-        if (enabled)
-        {
-            enableReceiver();
-        }
-    }
-}
-
-void setTxPower(int8_t aTxPower)
-{
-    uint8_t i = 0;
-
-    if (sTxPower != aTxPower)
-    {
-        otLogInfoPlat("TxPower=%d", aTxPower);
-
-        for (i = sizeof(sTxPowerTable) / sizeof(TxPowerTable) - 1; i > 0; i--)
-        {
-            if (aTxPower < sTxPowerTable[i].mTxPowerVal)
-            {
-                break;
-            }
-        }
-
-        HWREG(RFCORE_XREG_TXPOWER) = sTxPowerTable[i].mTxPowerReg;
-        sTxPower                   = aTxPower;
-    }
-}
-
-static bool cc2538SrcMatchEnabled(void)
-{
-    return (HWREG(RFCORE_XREG_FRMCTRL1) & RFCORE_XREG_FRMCTRL1_PENDING_OR) == 0;
-}
-
-static bool cc2538GetSrcMatchFoundIntFlag(void)
-{
-    bool flag = (HWREG(RFCORE_SFR_RFIRQF0) & RFCORE_SFR_RFIRQF0_SRC_MATCH_FOUND) != 0;
-    if (flag)
-    {
-        HWREG(RFCORE_SFR_RFIRQF0) &= ~RFCORE_SFR_RFIRQF0_SRC_MATCH_FOUND;
-    }
-    return flag;
-}
-
-void otPlatRadioGetIeeeEui64(otInstance *aInstance, uint8_t *aIeeeEui64)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    // EUI64 is in a mixed-endian format.  Split in two halves, each 32-bit
-    // half is in little-endian format (machine endian).  However, the
-    // most significant part of the EUI64 comes first, so we can't cheat
-    // with a uint64_t!
-    //
-    // See https://e2e.ti.com/support/wireless_connectivity/low_power_rf_tools/f/155/p/307344/1072252
-
-    volatile uint32_t *eui64 = &HWREG(IEEE_EUI64);
-
-    // Read first 32-bits
-    uint32_t part = eui64[0];
-    for (uint8_t i = 0; i < (OT_EXT_ADDRESS_SIZE / 2); i++)
-    {
-        aIeeeEui64[3 - i] = part;
-        part >>= 8;
-    }
-
-    // Read the last 32-bits
-    part = eui64[1];
-    for (uint8_t i = 0; i < (OT_EXT_ADDRESS_SIZE / 2); i++)
-    {
-        aIeeeEui64[7 - i] = part;
-        part >>= 8;
-    }
-}
-
-void otPlatRadioSetPanId(otInstance *aInstance, uint16_t aPanid)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otLogInfoPlat("PANID=%X", aPanid);
-
-    HWREG(RFCORE_FFSM_PAN_ID0) = aPanid & 0xFF;
-    HWREG(RFCORE_FFSM_PAN_ID1) = aPanid >> 8;
-}
-
-void otPlatRadioSetExtendedAddress(otInstance *aInstance, const otExtAddress *aAddress)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otLogInfoPlat("ExtAddr=%X%X%X%X%X%X%X%X", aAddress->m8[7], aAddress->m8[6], aAddress->m8[5], aAddress->m8[4],
-                  aAddress->m8[3], aAddress->m8[2], aAddress->m8[1], aAddress->m8[0]);
-
-    for (int i = 0; i < 8; i++)
-    {
-        ((volatile uint32_t *)RFCORE_FFSM_EXT_ADDR0)[i] = aAddress->m8[i];
-    }
-}
-
-void otPlatRadioSetShortAddress(otInstance *aInstance, uint16_t aAddress)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otLogInfoPlat("ShortAddr=%X", aAddress);
-
-    HWREG(RFCORE_FFSM_SHORT_ADDR0) = aAddress & 0xFF;
-    HWREG(RFCORE_FFSM_SHORT_ADDR1) = aAddress >> 8;
-}
-
-void cc2538RadioInit(void)
-{
-    sTransmitFrame.mLength = 0;
-    sTransmitFrame.mPsdu   = sTransmitPsdu;
-    sReceiveFrame.mLength  = 0;
-    sReceiveFrame.mPsdu    = sReceivePsdu;
-
-#if OPENTHREAD_CONFIG_CC2538_USE_RADIO_RX_INTERRUPT
-    // Enable interrupts for RX/TX, interrupt 26.
-    // That's NVIC index 0 (26 >> 5) bit 26 (26 & 0x1f).
-    HWREG(NVIC_EN0 + (0 * 4)) = (1 << 26);
-    HWREG(RFCORE_XREG_RFIRQM0) |= RFCORE_XREG_RFIRQM0_RXPKTDONE;
-#endif
-
-    // enable clock
-    HWREG(SYS_CTRL_RCGCRFC) = SYS_CTRL_RCGCRFC_RFC0;
-    HWREG(SYS_CTRL_SCGCRFC) = SYS_CTRL_SCGCRFC_RFC0;
-    HWREG(SYS_CTRL_DCGCRFC) = SYS_CTRL_DCGCRFC_RFC0;
-
-    // Table 23-7.
-    HWREG(RFCORE_XREG_AGCCTRL1)              = 0x15;
-    HWREG(RFCORE_XREG_TXFILTCFG)             = 0x09;
-    HWREG(ANA_REGS_BASE + ANA_REGS_O_IVCTRL) = 0x0b;
-
-    HWREG(RFCORE_XREG_CCACTRL0)  = 0xf8;
-    HWREG(RFCORE_XREG_FIFOPCTRL) = IEEE802154_MAX_LENGTH;
-
-    HWREG(RFCORE_XREG_FRMCTRL0) = RFCORE_XREG_FRMCTRL0_AUTOCRC | RFCORE_XREG_FRMCTRL0_AUTOACK;
-
-    // default: SRCMATCH.SRC_MATCH_EN(1), SRCMATCH.AUTOPEND(1),
-    // SRCMATCH.PEND_DATAREQ_ONLY(1), RFCORE_XREG_FRMCTRL1_PENDING_OR(0)
-
-    HWREG(RFCORE_XREG_TXPOWER) = sTxPowerTable[0].mTxPowerReg;
-    sTxPower                   = sTxPowerTable[0].mTxPowerVal;
-
-#if OPENTHREAD_CONFIG_CC2538_WITH_CC2592
-    // PA_EN pin configuration.
-    // Step 1. make it an output
-    HWREG(GPIO_C_BASE | GPIO_O_DIR) |= GPIO_PIN(OPENTHREAD_CONFIG_CC2592_PA_EN_PIN);
-    // Step 2. Route PA_PD to OBS0 and invert it to produce PA_EN
-    HWREG_ARR(RFCORE_XREG_RFC_OBS_CTRL, 0) = RFCORE_XREG_RFC_OBS_POL_INV      // Invert the output
-                                             | RFCORE_XREG_RFC_OBS_MUX_PA_PD; // PA "power down" signal
-    // Step 3. Connect the selected pin to OBS0 and enable OBS0.
-    HWREG_ARR(CCTEST_OBSSEL, OPENTHREAD_CONFIG_CC2592_PA_EN_PIN) = CCTEST_OBSSEL_EN          // Enable the output
-                                                                   | CCTEST_OBSSEL_SEL_OBS0; // Select OBS0
-
-    // LNA_EN pin configuration.
-    HWREG(GPIO_C_BASE | GPIO_O_DIR) |= GPIO_PIN(OPENTHREAD_CONFIG_CC2592_LNA_EN_PIN);
-    HWREG_ARR(RFCORE_XREG_RFC_OBS_CTRL, 1) = RFCORE_XREG_RFC_OBS_POL_INV | RFCORE_XREG_RFC_OBS_MUX_LNA_PD;
-    HWREG_ARR(CCTEST_OBSSEL, OPENTHREAD_CONFIG_CC2592_LNA_EN_PIN) = CCTEST_OBSSEL_EN | CCTEST_OBSSEL_SEL_OBS1;
-
-#if OPENTHREAD_CONFIG_CC2592_USE_HGM
-    // HGM pin configuration.  Set the pin state first so we don't glitch.
-    cc2538RadioSetHgm(OPENTHREAD_CONFIG_CC2592_HGM_DEFAULT_STATE);
-    HWREG(OPENTHREAD_CONFIG_CC2592_HGM_PORT | GPIO_O_DIR) |= GPIO_PIN(OPENTHREAD_CONFIG_CC2592_HGM_PIN);
-#endif // OPENTHREAD_CONFIG_CC2592_USE_HGM
-#endif // OPENTHREAD_CONFIG_CC2538_WITH_CC2592
-
-    otLogInfoPlat("Initialized", NULL);
-}
-
-#if OPENTHREAD_CONFIG_CC2538_WITH_CC2592 && OPENTHREAD_CONFIG_CC2592_USE_HGM
-void cc2538RadioSetHgm(bool aState)
-{
-    if (aState)
-    {
-        HWREG_ARR(OPENTHREAD_CONFIG_CC2592_HGM_PORT, GPIO_PIN(OPENTHREAD_CONFIG_CC2592_HGM_PIN)) =
-            GPIO_PIN(OPENTHREAD_CONFIG_CC2592_HGM_PIN);
-    }
-    else
-    {
-        HWREG_ARR(OPENTHREAD_CONFIG_CC2592_HGM_PORT, GPIO_PIN(OPENTHREAD_CONFIG_CC2592_HGM_PIN)) = 0;
-    }
-}
-
-bool cc2538RadioGetHgm(void)
-{
-    if (HWREG_ARR(OPENTHREAD_CONFIG_CC2592_HGM_PORT, GPIO_PIN(OPENTHREAD_CONFIG_CC2592_HGM_PIN)) &
-        GPIO_PIN(OPENTHREAD_CONFIG_CC2592_HGM_PIN))
-    {
-        return true;
-    }
-    else
-    {
-        return false;
-    }
-}
-#endif // OPENTHREAD_CONFIG_CC2538_WITH_CC2592 && OPENTHREAD_CONFIG_CC2592_USE_HGM
-
-bool otPlatRadioIsEnabled(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    return (sState != OT_RADIO_STATE_DISABLED) ? true : false;
-}
-
-otError otPlatRadioEnable(otInstance *aInstance)
-{
-    if (!otPlatRadioIsEnabled(aInstance))
-    {
-        otLogDebgPlat("State=OT_RADIO_STATE_SLEEP", NULL);
-        sState = OT_RADIO_STATE_SLEEP;
-    }
-
-    return OT_ERROR_NONE;
-}
-
-otError otPlatRadioDisable(otInstance *aInstance)
-{
-    if (otPlatRadioIsEnabled(aInstance))
-    {
-        otLogDebgPlat("State=OT_RADIO_STATE_DISABLED", NULL);
-        sState = OT_RADIO_STATE_DISABLED;
-    }
-
-    return OT_ERROR_NONE;
-}
-
-otError otPlatRadioSleep(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otError error = OT_ERROR_INVALID_STATE;
-
-    if (sState == OT_RADIO_STATE_SLEEP || sState == OT_RADIO_STATE_RECEIVE)
-    {
-        otLogDebgPlat("State=OT_RADIO_STATE_SLEEP", NULL);
-        error  = OT_ERROR_NONE;
-        sState = OT_RADIO_STATE_SLEEP;
-        disableReceiver();
-    }
-
-    return error;
-}
-
-otError otPlatRadioReceive(otInstance *aInstance, uint8_t aChannel)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otError error = OT_ERROR_INVALID_STATE;
-
-    if (sState != OT_RADIO_STATE_DISABLED)
-    {
-        otLogDebgPlat("State=OT_RADIO_STATE_RECEIVE", NULL);
-
-        error  = OT_ERROR_NONE;
-        sState = OT_RADIO_STATE_RECEIVE;
-        setChannel(aChannel);
-        sReceiveFrame.mChannel = aChannel;
-        enableReceiver();
-    }
-
-    return error;
-}
-
-static void setupTransmit(otRadioFrame *aFrame)
-{
-    int i;
-
-    // wait for current TX operation to complete, if any.
-    while (HWREG(RFCORE_XREG_FSMSTAT1) & RFCORE_XREG_FSMSTAT1_TX_ACTIVE)
-        ;
-
-    // flush txfifo
-    HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHTX;
-    HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHTX;
-
-    // frame length
-    HWREG(RFCORE_SFR_RFDATA) = aFrame->mLength;
-
-    // frame data
-    for (i = 0; i < aFrame->mLength; i++)
-    {
-        HWREG(RFCORE_SFR_RFDATA) = aFrame->mPsdu[i];
-    }
-
-    setChannel(aFrame->mChannel);
-}
-
-otError otPlatRadioTransmit(otInstance *aInstance, otRadioFrame *aFrame)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otError error = OT_ERROR_INVALID_STATE;
-
-    if (sState == OT_RADIO_STATE_RECEIVE)
-    {
-        int i;
-
-        error          = OT_ERROR_NONE;
-        sState         = OT_RADIO_STATE_TRANSMIT;
-        sTransmitError = OT_ERROR_NONE;
-
-        setupTransmit(aFrame);
-
-        // Set up a counter to inform us if we get stuck.
-        i = 1000000;
-
-        // Wait for radio to enter receive state.
-        while ((HWREG(RFCORE_XREG_FSMSTAT1) & RFCORE_XREG_FSMSTAT1_RX_ACTIVE) == 0)
-        {
-            // Count down the cycles, and emit a message if we get to zero.
-            // Ideally, we should never get there!
-            if (i)
-            {
-                i--;
-            }
-            else
-            {
-                otLogCritPlat("Radio is stuck!!! FSMSTAT0=0x%08x FSMSTAT1=0x%08x RFERRF=0x%08x",
-                              HWREG(RFCORE_XREG_FSMSTAT0), HWREG(RFCORE_XREG_FSMSTAT1), HWREG(RFCORE_SFR_RFERRF));
-                i = 1000000;
-            }
-
-            // Ensure we haven't overflowed the RX buffer in the mean time, as this
-            // will cause a deadlock here otherwise.  Similarly, if we see an aborted
-            // RX, handle that here too to prevent deadlock.
-            if (HWREG(RFCORE_SFR_RFERRF) & (RFCORE_SFR_RFERRF_RXOVERF | RFCORE_SFR_RFERRF_RXABO))
-            {
-                if (HWREG(RFCORE_SFR_RFERRF) & RFCORE_SFR_RFERRF_RXOVERF)
-                {
-                    otLogCritPlat("RX Buffer Overflow detected", NULL);
-                }
-
-                if (HWREG(RFCORE_SFR_RFERRF) & RFCORE_SFR_RFERRF_RXABO)
-                {
-                    otLogCritPlat("Aborted RX detected", NULL);
-                }
-
-                // Flush the RX buffer
-                HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHRX;
-                HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHRX;
-            }
-
-            // Check for idle state.  After flushing the RX buffer, we may wind up here.
-            if (!(HWREG(RFCORE_XREG_FSMSTAT1) & (RFCORE_XREG_FSMSTAT1_TX_ACTIVE | RFCORE_XREG_FSMSTAT1_RX_ACTIVE)))
-            {
-                otLogCritPlat("Idle state detected", NULL);
-
-                // In this case, the state of our driver mis-matches our state.  So force
-                // matters by clearing our channel variable and calling setChannel.  This
-                // should bring our radio into the RX state, which should allow us to go
-                // into TX.
-                sChannel = 0;
-                setupTransmit(aFrame);
-            }
-        }
-
-        // wait for valid rssi
-        while ((HWREG(RFCORE_XREG_RSSISTAT) & RFCORE_XREG_RSSISTAT_RSSI_VALID) == 0)
-            ;
-
-        otEXPECT_ACTION(((HWREG(RFCORE_XREG_FSMSTAT1) & RFCORE_XREG_FSMSTAT1_CCA) &&
-                         !((HWREG(RFCORE_XREG_FSMSTAT1) & RFCORE_XREG_FSMSTAT1_SFD))),
-                        sTransmitError = OT_ERROR_CHANNEL_ACCESS_FAILURE);
-
-        // begin transmit
-        HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_TXON;
-
-        otPlatRadioTxStarted(aInstance, aFrame);
-
-        while (HWREG(RFCORE_XREG_FSMSTAT1) & RFCORE_XREG_FSMSTAT1_TX_ACTIVE)
-            ;
-
-        otLogDebgPlat("Transmitted %d bytes", aFrame->mLength);
-    }
-
-exit:
-    return error;
-}
-
-otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    return &sTransmitFrame;
-}
-
-int8_t otPlatRadioGetRssi(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    int8_t rssi = OT_RADIO_RSSI_INVALID;
-
-    if ((HWREG(RFCORE_XREG_RSSISTAT) & RFCORE_XREG_RSSISTAT_RSSI_VALID) != 0)
-    {
-        rssi = HWREG(RFCORE_XREG_RSSI) & 0xff;
-
-        if (rssi > cc2538RadioGetRssiOffset() - 128)
-        {
-            rssi -= cc2538RadioGetRssiOffset();
-        }
-        else
-        {
-            rssi = -128;
-        }
-    }
-
-    return rssi;
-}
-
-otRadioCaps otPlatRadioGetCaps(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    return OT_RADIO_CAPS_ENERGY_SCAN;
-}
-
-static bool cc2538RadioGetPromiscuous(void)
-{
-    return (HWREG(RFCORE_XREG_FRMFILT0) & RFCORE_XREG_FRMFILT0_FRAME_FILTER_EN) == 0;
-}
-
-bool otPlatRadioGetPromiscuous(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    return cc2538RadioGetPromiscuous();
-}
-
-static int8_t cc2538RadioGetRssiOffset(void)
-{
-#if OPENTHREAD_CONFIG_CC2538_WITH_CC2592 && OPENTHREAD_CONFIG_CC2592_USE_HGM
-    if (cc2538RadioGetHgm())
-    {
-        return CC2592_RSSI_OFFSET_HGM;
-    }
-    else
-    {
-        return CC2592_RSSI_OFFSET_LGM;
-    }
-#else  // OPENTHREAD_CONFIG_CC2538_WITH_CC2592 && OPENTHREAD_CONFIG_CC2592_USE_HGM
-    return CC2538_RSSI_OFFSET;
-#endif // OPENTHREAD_CONFIG_CC2538_WITH_CC2592 && OPENTHREAD_CONFIG_CC2592_USE_HGM
-}
-
-void otPlatRadioSetPromiscuous(otInstance *aInstance, bool aEnable)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otLogInfoPlat("PromiscuousMode=%d", aEnable ? 1 : 0);
-
-    if (aEnable)
-    {
-        HWREG(RFCORE_XREG_FRMFILT0) &= ~RFCORE_XREG_FRMFILT0_FRAME_FILTER_EN;
-    }
-    else
-    {
-        HWREG(RFCORE_XREG_FRMFILT0) |= RFCORE_XREG_FRMFILT0_FRAME_FILTER_EN;
-    }
-}
-
-static void readFrame(void)
-{
-    uint8_t length;
-    uint8_t crcCorr;
-    int     i;
-
-    /*
-     * There is already a frame present in the buffer, return early so
-     * we do not overwrite it (hopefully we'll catch it on the next run).
-     */
-    otEXPECT(sReceiveFrame.mLength == 0);
-
-    otEXPECT(sState == OT_RADIO_STATE_RECEIVE || sState == OT_RADIO_STATE_TRANSMIT);
-    otEXPECT((HWREG(RFCORE_XREG_FSMSTAT1) & RFCORE_XREG_FSMSTAT1_FIFOP) != 0);
-
-    // read length
-    length = HWREG(RFCORE_SFR_RFDATA);
-    otEXPECT(IEEE802154_MIN_LENGTH <= length && length <= IEEE802154_MAX_LENGTH);
-
-#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-#error Time sync requires the timestamp of SFD rather than that of rx done!
-#else
-    // Timestamp
-    if (cc2538RadioGetPromiscuous())
-#endif
-    {
-        // The current driver only supports milliseconds resolution.
-        sReceiveFrame.mInfo.mRxInfo.mTimestamp = otPlatAlarmMilliGetNow() * 1000;
-    }
-
-    // read psdu
-    for (i = 0; i < length - 2; i++)
-    {
-        sReceiveFrame.mPsdu[i] = HWREG(RFCORE_SFR_RFDATA);
-    }
-
-    sReceiveFrame.mInfo.mRxInfo.mRssi = (int8_t)HWREG(RFCORE_SFR_RFDATA) - cc2538RadioGetRssiOffset();
-    crcCorr                           = HWREG(RFCORE_SFR_RFDATA);
-
-    if (crcCorr & CC2538_CRC_BIT_MASK)
-    {
-        sReceiveFrame.mLength            = length;
-        sReceiveFrame.mInfo.mRxInfo.mLqi = crcCorr & CC2538_LQI_BIT_MASK;
-
-        if (length > IEEE802154_ACK_LENGTH)
-        {
-            // Set ACK FP flag for the received frame according to whether SRC_MATCH_FOUND was triggered just before
-            // if SRC MATCH is not enabled, SRC_MATCH_FOUND is not triggered and all ACK FP is always set
-            sReceiveFrame.mInfo.mRxInfo.mAckedWithFramePending =
-                cc2538SrcMatchEnabled() ? cc2538GetSrcMatchFoundIntFlag() : true;
-        }
-    }
-    else
-    {
-        // resets rxfifo
-        HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHRX;
-        HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHRX;
-#if OPENTHREAD_CONFIG_LOG_PLATFORM && OPENTHREAD_CONFIG_CC2538_USE_RADIO_RX_INTERRUPT
-        // Debugging _and_ logging are enabled, it may not be safe to do
-        // logging if we're in the interrupt context, so just stash the
-        // length and do the logging later.
-        sDroppedFrameLength = length;
-#else
-        otLogDebgPlat("Dropping %d received bytes (Invalid CRC)", length);
-#endif
-    }
-
-    // check for rxfifo overflow
-    if ((HWREG(RFCORE_XREG_FSMSTAT1) & RFCORE_XREG_FSMSTAT1_FIFOP) != 0 &&
-        (HWREG(RFCORE_XREG_FSMSTAT1) & RFCORE_XREG_FSMSTAT1_FIFO) == 0)
-    {
-        HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHRX;
-        HWREG(RFCORE_SFR_RFST) = RFCORE_SFR_RFST_INSTR_FLUSHRX;
-    }
-
-exit:
-    return;
-}
-
-void cc2538RadioProcess(otInstance *aInstance)
-{
-#if OPENTHREAD_CONFIG_CC2538_USE_RADIO_RX_INTERRUPT
-    // Disable the receive interrupt so that sReceiveFrame doesn't get
-    // blatted by the interrupt handler while we're polling.
-    HWREG(RFCORE_XREG_RFIRQM0) &= ~RFCORE_XREG_RFIRQM0_RXPKTDONE;
-#endif
-
-    readFrame();
-
-#if OPENTHREAD_CONFIG_LOG_PLATFORM && OPENTHREAD_CONFIG_CC2538_USE_RADIO_RX_INTERRUPT
-    if (sDroppedFrameLength != 0)
-    {
-        otLogDebgPlat("Dropping %d received bytes (Invalid CRC)", sDroppedFrameLength);
-        sDroppedFrameLength = 0;
-    }
-#endif
-
-    if ((sState == OT_RADIO_STATE_RECEIVE && sReceiveFrame.mLength > 0) ||
-        (sState == OT_RADIO_STATE_TRANSMIT && sReceiveFrame.mLength > IEEE802154_ACK_LENGTH))
-    {
-#if OPENTHREAD_CONFIG_DIAG_ENABLE
-
-        if (otPlatDiagModeGet())
-        {
-            otPlatDiagRadioReceiveDone(aInstance, &sReceiveFrame, sReceiveError);
-        }
-        else
-#endif
-        {
-            // signal MAC layer for each received frame if promiscuous is enabled
-            // otherwise only signal MAC layer for non-ACK frame
-            if (((HWREG(RFCORE_XREG_FRMFILT0) & RFCORE_XREG_FRMFILT0_FRAME_FILTER_EN) == 0) ||
-                (sReceiveFrame.mLength > IEEE802154_ACK_LENGTH))
-            {
-                otLogDebgPlat("Received %d bytes", sReceiveFrame.mLength);
-                otPlatRadioReceiveDone(aInstance, &sReceiveFrame, sReceiveError);
-            }
-        }
-    }
-
-    if (sState == OT_RADIO_STATE_TRANSMIT)
-    {
-        if (sTransmitError != OT_ERROR_NONE || (sTransmitFrame.mPsdu[0] & IEEE802154_ACK_REQUEST) == 0)
-        {
-            if (sTransmitError != OT_ERROR_NONE)
-            {
-                otLogDebgPlat("Transmit failed ErrorCode=%d", sTransmitError);
-            }
-
-            sState = OT_RADIO_STATE_RECEIVE;
-
-#if OPENTHREAD_CONFIG_DIAG_ENABLE
-
-            if (otPlatDiagModeGet())
-            {
-                otPlatDiagRadioTransmitDone(aInstance, &sTransmitFrame, sTransmitError);
-            }
-            else
-#endif
-            {
-                otPlatRadioTxDone(aInstance, &sTransmitFrame, NULL, sTransmitError);
-            }
-        }
-        else if (sReceiveFrame.mLength == IEEE802154_ACK_LENGTH &&
-                 (sReceiveFrame.mPsdu[0] & IEEE802154_FRAME_TYPE_MASK) == IEEE802154_FRAME_TYPE_ACK &&
-                 (sReceiveFrame.mPsdu[IEEE802154_DSN_OFFSET] == sTransmitFrame.mPsdu[IEEE802154_DSN_OFFSET]))
-        {
-            sState = OT_RADIO_STATE_RECEIVE;
-
-            otPlatRadioTxDone(aInstance, &sTransmitFrame, &sReceiveFrame, sTransmitError);
-        }
-    }
-
-    sReceiveFrame.mLength = 0;
-
-#if OPENTHREAD_CONFIG_CC2538_USE_RADIO_RX_INTERRUPT
-    // Turn the receive interrupt handler back on now the buffer is clear.
-    HWREG(RFCORE_XREG_RFIRQM0) |= RFCORE_XREG_RFIRQM0_RXPKTDONE;
-#endif
-}
-
-void RFCoreRxTxIntHandler(void)
-{
-#if OPENTHREAD_CONFIG_CC2538_USE_RADIO_RX_INTERRUPT
-    if (HWREG(RFCORE_SFR_RFIRQF0) & RFCORE_SFR_RFIRQF0_RXPKTDONE)
-    {
-        readFrame();
-
-        if (sReceiveFrame.mLength > 0)
-        {
-            // A frame has been received, disable the interrupt handler
-            // until the main loop has dealt with this previous frame,
-            // otherwise we might overwrite it whilst it is being read.
-            HWREG(RFCORE_XREG_RFIRQM0) &= ~RFCORE_XREG_RFIRQM0_RXPKTDONE;
-        }
-    }
-#endif
-
-    HWREG(RFCORE_SFR_RFIRQF0) = 0;
-}
-
-void RFCoreErrIntHandler(void)
-{
-    HWREG(RFCORE_SFR_RFERRF) = 0;
-}
-
-uint32_t getSrcMatchEntriesEnableStatus(bool aShort)
-{
-    uint32_t  status = 0;
-    uint32_t *addr   = aShort ? (uint32_t *)RFCORE_XREG_SRCSHORTEN0 : (uint32_t *)RFCORE_XREG_SRCEXTEN0;
-
-    for (uint8_t i = 0; i < RFCORE_XREG_SRCMATCH_ENABLE_STATUS_SIZE; i++)
-    {
-        status |= HWREG(addr++) << (i * 8);
-    }
-
-    return status;
-}
-
-int8_t findSrcMatchShortEntry(uint16_t aShortAddress)
-{
-    int8_t    entry = -1;
-    uint16_t  shortAddr;
-    uint32_t  bitMask;
-    uint32_t *addr   = NULL;
-    uint32_t  status = getSrcMatchEntriesEnableStatus(true);
-
-    for (uint8_t i = 0; i < RFCORE_XREG_SRCMATCH_SHORT_ENTRIES; i++)
-    {
-        bitMask = 0x00000001 << i;
-
-        if ((status & bitMask) == 0)
-        {
-            continue;
-        }
-
-        addr = (uint32_t *)RFCORE_FFSM_SRCADDRESS_TABLE + (i * RFCORE_XREG_SRCMATCH_SHORT_ENTRY_OFFSET);
-
-        shortAddr = HWREG(addr + 2);
-        shortAddr |= HWREG(addr + 3) << 8;
-
-        if ((shortAddr == aShortAddress))
-        {
-            entry = i;
-            break;
-        }
-    }
-
-    return entry;
-}
-
-int8_t findSrcMatchExtEntry(const otExtAddress *aExtAddress)
-{
-    int8_t    entry = -1;
-    uint32_t  bitMask;
-    uint32_t *addr   = NULL;
-    uint32_t  status = getSrcMatchEntriesEnableStatus(false);
-
-    for (uint8_t i = 0; i < RFCORE_XREG_SRCMATCH_EXT_ENTRIES; i++)
-    {
-        uint8_t j = 0;
-        bitMask   = 0x00000001 << 2 * i;
-
-        if ((status & bitMask) == 0)
-        {
-            continue;
-        }
-
-        addr = (uint32_t *)RFCORE_FFSM_SRCADDRESS_TABLE + (i * RFCORE_XREG_SRCMATCH_EXT_ENTRY_OFFSET);
-
-        for (j = 0; j < sizeof(otExtAddress); j++)
-        {
-            if (HWREG(addr + j) != aExtAddress->m8[j])
-            {
-                break;
-            }
-        }
-
-        if (j == sizeof(otExtAddress))
-        {
-            entry = i;
-            break;
-        }
-    }
-
-    return entry;
-}
-
-void setSrcMatchEntryEnableStatus(bool aShort, uint8_t aEntry, bool aEnable)
-{
-    uint8_t   entry          = aShort ? aEntry : (2 * aEntry);
-    uint8_t   index          = entry / 8;
-    uint32_t *addrEn         = aShort ? (uint32_t *)RFCORE_XREG_SRCSHORTEN0 : (uint32_t *)RFCORE_XREG_SRCEXTEN0;
-    uint32_t *addrAutoPendEn = aShort ? (uint32_t *)RFCORE_FFSM_SRCSHORTPENDEN0 : (uint32_t *)RFCORE_FFSM_SRCEXTPENDEN0;
-    uint32_t  bitMask        = 0x00000001;
-
-    if (aEnable)
-    {
-        HWREG(addrEn + index) |= (bitMask) << (entry % 8);
-        HWREG(addrAutoPendEn + index) |= (bitMask) << (entry % 8);
-    }
-    else
-    {
-        HWREG(addrEn + index) &= ~((bitMask) << (entry % 8));
-        HWREG(addrAutoPendEn + index) &= ~((bitMask) << (entry % 8));
-    }
-}
-
-int8_t findSrcMatchAvailEntry(bool aShort)
-{
-    int8_t   entry = -1;
-    uint32_t bitMask;
-    uint32_t shortEnableStatus = getSrcMatchEntriesEnableStatus(true);
-    uint32_t extEnableStatus   = getSrcMatchEntriesEnableStatus(false);
-
-    otLogDebgPlat("Short enable status: 0x%x", shortEnableStatus);
-    otLogDebgPlat("Ext enable status: 0x%x", extEnableStatus);
-
-    if (aShort)
-    {
-        bitMask = 0x00000001;
-
-        for (uint8_t i = 0; i < RFCORE_XREG_SRCMATCH_SHORT_ENTRIES; i++)
-        {
-            if ((extEnableStatus & bitMask) == 0)
-            {
-                if ((shortEnableStatus & bitMask) == 0)
-                {
-                    entry = i;
-                    break;
-                }
-            }
-
-            if (i % 2 == 1)
-            {
-                extEnableStatus = extEnableStatus >> 2;
-            }
-
-            shortEnableStatus = shortEnableStatus >> 1;
-        }
-    }
-    else
-    {
-        bitMask = 0x00000003;
-
-        for (uint8_t i = 0; i < RFCORE_XREG_SRCMATCH_EXT_ENTRIES; i++)
-        {
-            if (((extEnableStatus | shortEnableStatus) & bitMask) == 0)
-            {
-                entry = i;
-                break;
-            }
-
-            extEnableStatus   = extEnableStatus >> 2;
-            shortEnableStatus = shortEnableStatus >> 2;
-        }
-    }
-
-    return entry;
-}
-
-void cc2538EnergyScanTimerHandler(void)
-{
-    int8_t rssi = otPlatRadioGetRssi(sInstance);
-
-    disableReceiver();
-
-    HWREG(RFCORE_XREG_FRMCTRL0) &= ~RFCORE_XREG_FRMCTRL0_ENERGY_SCAN;
-    HWREG(RFCORE_XREG_FREQCTRL) = 11 + (sChannel - 11) * 5;
-
-    if (sIsReceiverEnabled)
-    {
-        enableReceiver();
-    }
-
-    otPlatRadioEnergyScanDone(sInstance, rssi);
-}
-
-void otPlatRadioEnableSrcMatch(otInstance *aInstance, bool aEnable)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otLogInfoPlat("EnableSrcMatch=%d", aEnable ? 1 : 0);
-
-    if (aEnable)
-    {
-        // only set FramePending when ack for data poll if there are queued messages
-        // for entries in the source match table.
-        HWREG(RFCORE_XREG_FRMCTRL1) &= ~RFCORE_XREG_FRMCTRL1_PENDING_OR;
-    }
-    else
-    {
-        // set FramePending for all ack.
-        HWREG(RFCORE_XREG_FRMCTRL1) |= RFCORE_XREG_FRMCTRL1_PENDING_OR;
-    }
-}
-
-otError otPlatRadioAddSrcMatchShortEntry(otInstance *aInstance, uint16_t aShortAddress)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otError   error = OT_ERROR_NONE;
-    int8_t    entry = findSrcMatchAvailEntry(true);
-    uint32_t *addr  = (uint32_t *)RFCORE_FFSM_SRCADDRESS_TABLE;
-
-    otLogDebgPlat("Add ShortAddr entry: %d", entry);
-
-    otEXPECT_ACTION(entry >= 0, error = OT_ERROR_NO_BUFS);
-
-    addr += (entry * RFCORE_XREG_SRCMATCH_SHORT_ENTRY_OFFSET);
-
-    HWREG(addr++) = HWREG(RFCORE_FFSM_PAN_ID0);
-    HWREG(addr++) = HWREG(RFCORE_FFSM_PAN_ID1);
-    HWREG(addr++) = aShortAddress & 0xFF;
-    HWREG(addr++) = aShortAddress >> 8;
-
-    setSrcMatchEntryEnableStatus(true, (uint8_t)(entry), true);
-
-exit:
-    return error;
-}
-
-otError otPlatRadioAddSrcMatchExtEntry(otInstance *aInstance, const otExtAddress *aExtAddress)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otError   error = OT_ERROR_NONE;
-    int8_t    entry = findSrcMatchAvailEntry(false);
-    uint32_t *addr  = (uint32_t *)RFCORE_FFSM_SRCADDRESS_TABLE;
-
-    otLogDebgPlat("Add ExtAddr entry: %d", entry);
-
-    otEXPECT_ACTION(entry >= 0, error = OT_ERROR_NO_BUFS);
-
-    addr += (entry * RFCORE_XREG_SRCMATCH_EXT_ENTRY_OFFSET);
-
-    for (uint8_t i = 0; i < sizeof(otExtAddress); i++)
-    {
-        HWREG(addr++) = aExtAddress->m8[i];
-    }
-
-    setSrcMatchEntryEnableStatus(false, (uint8_t)(entry), true);
-
-exit:
-    return error;
-}
-
-otError otPlatRadioClearSrcMatchShortEntry(otInstance *aInstance, uint16_t aShortAddress)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otError error = OT_ERROR_NONE;
-    int8_t  entry = findSrcMatchShortEntry(aShortAddress);
-
-    otLogDebgPlat("Clear ShortAddr entry: %d", entry);
-
-    otEXPECT_ACTION(entry >= 0, error = OT_ERROR_NO_ADDRESS);
-
-    setSrcMatchEntryEnableStatus(true, (uint8_t)(entry), false);
-
-exit:
-    return error;
-}
-
-otError otPlatRadioClearSrcMatchExtEntry(otInstance *aInstance, const otExtAddress *aExtAddress)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otError error = OT_ERROR_NONE;
-    int8_t  entry = findSrcMatchExtEntry(aExtAddress);
-
-    otLogDebgPlat("Clear ExtAddr entry: %d", entry);
-
-    otEXPECT_ACTION(entry >= 0, error = OT_ERROR_NO_ADDRESS);
-
-    setSrcMatchEntryEnableStatus(false, (uint8_t)(entry), false);
-
-exit:
-    return error;
-}
-
-void otPlatRadioClearSrcMatchShortEntries(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    uint32_t *addrEn         = (uint32_t *)RFCORE_XREG_SRCSHORTEN0;
-    uint32_t *addrAutoPendEn = (uint32_t *)RFCORE_FFSM_SRCSHORTPENDEN0;
-
-    otLogDebgPlat("Clear ShortAddr entries", NULL);
-
-    for (uint8_t i = 0; i < RFCORE_XREG_SRCMATCH_ENABLE_STATUS_SIZE; i++)
-    {
-        HWREG(addrEn++)         = 0;
-        HWREG(addrAutoPendEn++) = 0;
-    }
-}
-
-void otPlatRadioClearSrcMatchExtEntries(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    uint32_t *addrEn         = (uint32_t *)RFCORE_XREG_SRCEXTEN0;
-    uint32_t *addrAutoPendEn = (uint32_t *)RFCORE_FFSM_SRCEXTPENDEN0;
-
-    otLogDebgPlat("Clear ExtAddr entries", NULL);
-
-    for (uint8_t i = 0; i < RFCORE_XREG_SRCMATCH_ENABLE_STATUS_SIZE; i++)
-    {
-        HWREG(addrEn++)         = 0;
-        HWREG(addrAutoPendEn++) = 0;
-    }
-}
-
-otError otPlatRadioEnergyScan(otInstance *aInstance, uint8_t aScanChannel, uint16_t aScanDuration)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otLogInfoPlat("ScanChannel=%d", aScanChannel);
-
-    if (aScanChannel != sChannel)
-    {
-        if (sIsReceiverEnabled)
-        {
-            disableReceiver();
-        }
-
-        HWREG(RFCORE_XREG_FREQCTRL) = 11 + (aScanChannel - 11) * 5;
-
-        enableReceiver();
-    }
-    else if (!sIsReceiverEnabled)
-    {
-        enableReceiver();
-    }
-
-    // Collect peak signal strength
-    HWREG(RFCORE_XREG_FRMCTRL0) |= RFCORE_XREG_FRMCTRL0_ENERGY_SCAN;
-
-    cc2538SetTimer(OT_CC2538_TIMER_ENERGY_SCAN, aScanDuration);
-
-    return OT_ERROR_NONE;
-}
-
-otError otPlatRadioGetTransmitPower(otInstance *aInstance, int8_t *aPower)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    otError error = OT_ERROR_NONE;
-
-    otEXPECT_ACTION(aPower != NULL, error = OT_ERROR_INVALID_ARGS);
-    *aPower = sTxPower;
-
-exit:
-    return error;
-}
-
-otError otPlatRadioSetTransmitPower(otInstance *aInstance, int8_t aPower)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-    setTxPower(aPower);
-    return OT_ERROR_NONE;
-}
-
-otError otPlatRadioGetCcaEnergyDetectThreshold(otInstance *aInstance, int8_t *aThreshold)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-    OT_UNUSED_VARIABLE(aThreshold);
-
-    return OT_ERROR_NOT_IMPLEMENTED;
-}
-
-otError otPlatRadioSetCcaEnergyDetectThreshold(otInstance *aInstance, int8_t aThreshold)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-    OT_UNUSED_VARIABLE(aThreshold);
-
-    return OT_ERROR_NOT_IMPLEMENTED;
-}
-
-int8_t otPlatRadioGetReceiveSensitivity(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-
-#if OPENTHREAD_CONFIG_CC2538_WITH_CC2592 && OPENTHREAD_CONFIG_CC2592_USE_HGM
-    if (cc2538RadioGetHgm())
-    {
-        return CC2592_RECEIVE_SENSITIVITY_HGM;
-    }
-    else
-    {
-        return CC2592_RECEIVE_SENSITIVITY_LGM;
-    }
-#else  // OPENTHREAD_CONFIG_CC2538_WITH_CC2592 && OPENTHREAD_CONFIG_CC2592_USE_HGM
-    return CC2538_RECEIVE_SENSITIVITY;
-#endif // OPENTHREAD_CONFIG_CC2538_WITH_CC2592 && OPENTHREAD_CONFIG_CC2592_USE_HGM
-}
diff --git a/examples/platforms/cc2538/rom-utility.h b/examples/platforms/cc2538/rom-utility.h
deleted file mode 100644
index d0686ef..0000000
--- a/examples/platforms/cc2538/rom-utility.h
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- *  Copyright (c) 2016, 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.
- */
-
-#ifndef ROM_UTILITY_H_
-#define ROM_UTILITY_H_
-
-#define ROM_API_TABLE_ADDR 0x00000048
-
-typedef uint32_t (*volatile FPTR_CRC32_T)(uint8_t * /*pData*/, uint32_t /*byteCount*/);
-typedef uint32_t (*volatile FPTR_GETFLSIZE_T)(void);
-typedef uint32_t (*volatile FPTR_GETCHIPID_T)(void);
-typedef int32_t (*volatile FPTR_PAGEERASE_T)(uint32_t /*FlashAddr*/, uint32_t /*Size*/);
-typedef int32_t (*volatile FPTR_PROGFLASH_T)(uint32_t * /*pRamData*/, uint32_t /*FlashAdr*/, uint32_t /*ByteCount*/);
-typedef void (*volatile FPTR_RESETDEV_T)(void);
-typedef void *(*volatile FPTR_MEMSET_T)(void * /*s*/, int32_t /*c*/, uint32_t /*n*/);
-typedef void *(*volatile FPTR_MEMCPY_T)(void * /*s1*/, const void * /*s2*/, uint32_t /*n*/);
-typedef int32_t (*volatile FPTR_MEMCMP_T)(const void * /*s1*/, const void * /*s2*/, uint32_t /*n*/);
-typedef void *(*volatile FPTR_MEMMOVE_T)(void * /*s1*/, const void * /*s2*/, uint32_t /*n*/);
-
-typedef struct
-{
-    FPTR_CRC32_T     Crc32;
-    FPTR_GETFLSIZE_T GetFlashSize;
-    FPTR_GETCHIPID_T GetChipId;
-    FPTR_PAGEERASE_T PageErase;
-    FPTR_PROGFLASH_T ProgramFlash;
-    FPTR_RESETDEV_T  ResetDevice;
-    FPTR_MEMSET_T    memset;
-    FPTR_MEMCPY_T    memcpy;
-    FPTR_MEMCMP_T    memcmp;
-    FPTR_MEMMOVE_T   memmove;
-} ROM_API_T;
-
-// clang-format off
-
-#define P_ROM_API               ((ROM_API_T*)ROM_API_TABLE_ADDR)
-
-#define ROM_Crc32(a,b)          P_ROM_API->Crc32(a,b)
-#define ROM_GetFlashSize()      P_ROM_API->GetFlashSize()
-#define ROM_GetChipId()         P_ROM_API->GetChipId()
-#define ROM_PageErase(a,b)      P_ROM_API->PageErase(a,b)
-#define ROM_ProgramFlash(a,b,c) P_ROM_API->ProgramFlash(a,b,c)
-#define ROM_ResetDevice()       P_ROM_API->ResetDevice()
-#define ROM_Memset(a,b,c)       P_ROM_API->memset(a,b,c)
-#define ROM_Memcpy(a,b,c)       P_ROM_API->memcpy(a,b,c)
-#define ROM_Memcmp(a,b,c)       P_ROM_API->memcmp(a,b,c)
-#define ROM_Memmove(a,b,c)      P_ROM_API->memmove(a,b,c)
-
-// clang-format on
-
-#endif // ROM_UTILITY_H_
diff --git a/examples/platforms/cc2538/startup-gcc.c b/examples/platforms/cc2538/startup-gcc.c
deleted file mode 100644
index f174bfa..0000000
--- a/examples/platforms/cc2538/startup-gcc.c
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 gcc-specific startup code for the cc2538.
- */
-
-#include <stdint.h>
-#include <string.h>
-
-#include "cc2538-reg.h"
-
-extern uint8_t _ldata;
-extern uint8_t _data;
-extern uint8_t _edata;
-extern uint8_t _bss;
-extern uint8_t _ebss;
-extern uint8_t _init_array;
-extern uint8_t _einit_array;
-
-__extension__ typedef int __guard __attribute__((mode(__DI__)));
-
-int __cxa_guard_acquire(__guard *g)
-{
-    return !*(char *)(g);
-}
-
-void __cxa_guard_release(__guard *g)
-{
-    *(char *)g = 1;
-}
-
-void __cxa_guard_abort(__guard *g)
-{
-    (void)g;
-}
-
-void __cxa_pure_virtual(void)
-{
-    while (1)
-        ;
-}
-
-void IntDefaultHandler(void);
-void ResetHandler(void);
-
-extern void SysTick_Handler(void);
-extern void UART0IntHandler(void);
-extern void RFCoreRxTxIntHandler(void);
-extern void RFCoreErrIntHandler(void);
-extern void main(void);
-
-static uint64_t stack[640] __attribute__((section(".stack")));
-
-__attribute__((section(".vectors"), used)) void (*const vectors[])(void) = {
-    (void (*)(void))((unsigned long)stack + sizeof(stack)), // Initial Stack Pointer
-    ResetHandler,                                           // 1 The reset handler
-    ResetHandler,                                           // 2 The NMI handler
-    IntDefaultHandler,                                      // 3 The hard fault handler
-    IntDefaultHandler,                                      // 4 The MPU fault handler
-    IntDefaultHandler,                                      // 5 The bus fault handler
-    IntDefaultHandler,                                      // 6 The usage fault handler
-    0,                                                      // 7 Reserved
-    0,                                                      // 8 Reserved
-    0,                                                      // 9 Reserved
-    0,                                                      // 10 Reserved
-    IntDefaultHandler,                                      // 11 SVCall handler
-    IntDefaultHandler,                                      // 12 Debug monitor handler
-    0,                                                      // 13 Reserved
-    IntDefaultHandler,                                      // 14 The PendSV handler
-    SysTick_Handler,                                        // 15 The SysTick handler
-    IntDefaultHandler,                                      // 16 GPIO Port A
-    IntDefaultHandler,                                      // 17 GPIO Port B
-    IntDefaultHandler,                                      // 18 GPIO Port C
-    IntDefaultHandler,                                      // 19 GPIO Port D
-    0,                                                      // 20 none
-    UART0IntHandler,                                        // 21 UART0 Rx and Tx
-    IntDefaultHandler,                                      // 22 UART1 Rx and Tx
-    IntDefaultHandler,                                      // 23 SSI0 Rx and Tx
-    IntDefaultHandler,                                      // 24 I2C Master and Slave
-    0,                                                      // 25 Reserved
-    0,                                                      // 26 Reserved
-    0,                                                      // 27 Reserved
-    0,                                                      // 28 Reserved
-    0,                                                      // 29 Reserved
-    IntDefaultHandler,                                      // 30 ADC Sequence 0
-    0,                                                      // 31 Reserved
-    0,                                                      // 32 Reserved
-    0,                                                      // 33 Reserved
-    IntDefaultHandler,                                      // 34 Watchdog timer, timer 0
-    IntDefaultHandler,                                      // 35 Timer 0 subtimer A
-    IntDefaultHandler,                                      // 36 Timer 0 subtimer B
-    IntDefaultHandler,                                      // 37 Timer 1 subtimer A
-    IntDefaultHandler,                                      // 38 Timer 1 subtimer B
-    IntDefaultHandler,                                      // 39 Timer 2 subtimer A
-    IntDefaultHandler,                                      // 40 Timer 2 subtimer B
-    IntDefaultHandler,                                      // 41 Analog Comparator 0
-    RFCoreRxTxIntHandler,                                   // 42 RFCore Rx/Tx
-    RFCoreErrIntHandler,                                    // 43 RFCore Error
-    IntDefaultHandler,                                      // 44 IcePick
-    IntDefaultHandler,                                      // 45 FLASH Control
-    IntDefaultHandler,                                      // 46 AES
-    IntDefaultHandler,                                      // 47 PKA
-    IntDefaultHandler,                                      // 48 Sleep Timer
-    IntDefaultHandler,                                      // 49 MacTimer
-    IntDefaultHandler,                                      // 50 SSI1 Rx and Tx
-    IntDefaultHandler,                                      // 51 Timer 3 subtimer A
-    IntDefaultHandler,                                      // 52 Timer 3 subtimer B
-    0,                                                      // 53 Reserved
-    0,                                                      // 54 Reserved
-    0,                                                      // 55 Reserved
-    0,                                                      // 56 Reserved
-    0,                                                      // 57 Reserved
-    0,                                                      // 58 Reserved
-    0,                                                      // 59 Reserved
-    IntDefaultHandler,                                      // 60 USB 2538
-    0,                                                      // 61 Reserved
-    IntDefaultHandler,                                      // 62 uDMA
-    IntDefaultHandler,                                      // 63 uDMA Error
-};
-
-void IntDefaultHandler(void)
-{
-    while (1)
-        ;
-}
-
-// clang-format off
-
-#define FLASH_CCA_BOOTLDR_CFG_DISABLE               0xEFFFFFFF ///< Disable backdoor function
-#define FLASH_CCA_BOOTLDR_CFG_ENABLE                0xF0FFFFFF ///< Enable backdoor function
-#define FLASH_CCA_BOOTLDR_CFG_ACTIVE_HIGH           0x08000000 ///< Selected pin on pad A active high
-#define FLASH_CCA_BOOTLDR_CFG_PORT_A_PIN_M          0x07000000 ///< Selected pin on pad A mask
-#define FLASH_CCA_BOOTLDR_CFG_PORT_A_PIN_S          24         ///< Selected pin on pad A shift
-#define FLASH_CCA_IMAGE_VALID                       0x00000000 ///< Indicates valid image in flash
-
-#define FLASH_CCA_CONF_BOOTLDR_BACKDOOR_PORT_A_PIN  3      ///< Select Button on SmartRF06 Eval Board
-
-// clang-format on
-
-typedef struct
-{
-    uint32_t ui32BootldrCfg;
-    uint32_t ui32ImageValid;
-    uint32_t ui32ImageVectorAddr;
-    uint8_t  ui8lock[32];
-} flash_cca_lock_page_t;
-
-__attribute__((__section__(".flashcca"), used)) const flash_cca_lock_page_t flash_cca_lock_page = {
-    FLASH_CCA_BOOTLDR_CFG_ENABLE | (FLASH_CCA_CONF_BOOTLDR_BACKDOOR_PORT_A_PIN << FLASH_CCA_BOOTLDR_CFG_PORT_A_PIN_S),
-    FLASH_CCA_IMAGE_VALID,
-    (uint32_t)&vectors,
-    {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
-     0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
-
-typedef void (*init_fn_t)(void);
-
-void ResetHandler(void)
-{
-    HWREG(SYS_CTRL_EMUOVR) = 0xFF;
-
-    // configure clocks
-    HWREG(SYS_CTRL_CLOCK_CTRL) |= SYS_CTRL_CLOCK_CTRL_AMP_DET;
-    HWREG(SYS_CTRL_CLOCK_CTRL) = SYS_CTRL_SYSDIV_32MHZ;
-
-    // alternate map
-    HWREG(SYS_CTRL_I_MAP) |= SYS_CTRL_I_MAP_ALTMAP;
-
-    // copy the data segment initializers from flash to SRAM
-    memcpy(&_data, &_ldata, &_edata - &_data);
-
-    // zero-fill the bss segment
-    memset(&_bss, 0, &_ebss - &_bss);
-
-    // C++ runtime initialization (BSS, Data, relocation, etc.)
-    init_fn_t *fp;
-
-    for (fp = (init_fn_t *)&_init_array; fp < (init_fn_t *)&_einit_array; fp++)
-    {
-        (*fp)();
-    }
-
-    // call the application's entry point
-    main();
-
-    // end here if main() returns
-    while (1)
-        ;
-}
diff --git a/examples/platforms/cc2538/uart.c b/examples/platforms/cc2538/uart.c
deleted file mode 100644
index 3325c99..0000000
--- a/examples/platforms/cc2538/uart.c
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 the OpenThread platform abstraction for UART communication.
- *
- */
-
-#include <openthread-core-config.h>
-#include <openthread/config.h>
-
-#include <stdarg.h>
-#include <stddef.h>
-#include <stdio.h>
-
-#include <openthread/platform/debug_uart.h>
-#include <openthread/platform/logging.h>
-
-#include "platform-cc2538.h"
-#include "utils/code_utils.h"
-#include "utils/uart.h"
-
-enum
-{
-    kPlatformClock     = 32000000,
-    kBaudRate          = 115200,
-    kReceiveBufferSize = 128,
-};
-
-extern void UART0IntHandler(void);
-
-static void processReceive(void);
-static void processTransmit(void);
-
-static const uint8_t *sTransmitBuffer = NULL;
-static uint16_t       sTransmitLength = 0;
-
-typedef struct RecvBuffer
-{
-    // The data buffer
-    uint8_t mBuffer[kReceiveBufferSize];
-    // The offset of the first item written to the list.
-    uint16_t mHead;
-    // The offset of the next item to be written to the list.
-    uint16_t mTail;
-} RecvBuffer;
-
-static RecvBuffer sReceive;
-
-static void enable_uart_clocks(void)
-{
-    static int uart_clocks_done = 0;
-
-    if (uart_clocks_done)
-    {
-        return;
-    }
-
-    uart_clocks_done = 1;
-
-#if OPENTHREAD_CONFIG_ENABLE_DEBUG_UART
-    HWREG(SYS_CTRL_RCGCUART) = (SYS_CTRL_RCGCUART_UART0 | SYS_CTRL_RCGCUART_UART1);
-    HWREG(SYS_CTRL_SCGCUART) = (SYS_CTRL_SCGCUART_UART0 | SYS_CTRL_SCGCUART_UART1);
-    HWREG(SYS_CTRL_DCGCUART) = (SYS_CTRL_DCGCUART_UART0 | SYS_CTRL_DCGCUART_UART1);
-#else
-    HWREG(SYS_CTRL_RCGCUART) = SYS_CTRL_RCGCUART_UART0;
-    HWREG(SYS_CTRL_SCGCUART) = SYS_CTRL_SCGCUART_UART0;
-    HWREG(SYS_CTRL_DCGCUART) = SYS_CTRL_DCGCUART_UART0;
-#endif
-}
-
-otError otPlatUartEnable(void)
-{
-    uint32_t div;
-
-    sReceive.mHead = 0;
-    sReceive.mTail = 0;
-
-    // clock
-    enable_uart_clocks();
-
-    HWREG(UART0_BASE + UART_O_CC) = 0;
-
-    // tx pin
-    HWREG(IOC_PA1_SEL)  = IOC_MUX_OUT_SEL_UART0_TXD;
-    HWREG(IOC_PA1_OVER) = IOC_OVERRIDE_OE;
-    HWREG(GPIO_A_BASE + GPIO_O_AFSEL) |= GPIO_PIN_1;
-
-    // rx pin
-    HWREG(IOC_UARTRXD_UART0) = IOC_PAD_IN_SEL_PA0;
-    HWREG(IOC_PA0_OVER)      = IOC_OVERRIDE_DIS;
-    HWREG(GPIO_A_BASE + GPIO_O_AFSEL) |= GPIO_PIN_0;
-
-    HWREG(UART0_BASE + UART_O_CTL) = 0;
-
-    // baud rate
-    div                             = (((kPlatformClock * 8) / kBaudRate) + 1) / 2;
-    HWREG(UART0_BASE + UART_O_IBRD) = div / 64;
-    HWREG(UART0_BASE + UART_O_FBRD) = div % 64;
-    HWREG(UART0_BASE + UART_O_LCRH) = UART_CONFIG_WLEN_8 | UART_CONFIG_STOP_ONE | UART_CONFIG_PAR_NONE;
-
-    // configure interrupts
-    HWREG(UART0_BASE + UART_O_IM) |= UART_IM_RXIM | UART_IM_RTIM;
-
-    // enable
-    HWREG(UART0_BASE + UART_O_CTL) = UART_CTL_UARTEN | UART_CTL_TXE | UART_CTL_RXE;
-
-    // enable interrupts
-    HWREG(NVIC_EN0) = 1 << ((INT_UART0 - 16) & 31);
-
-    return OT_ERROR_NONE;
-}
-
-otError otPlatUartDisable(void)
-{
-    return OT_ERROR_NONE;
-}
-
-otError otPlatUartSend(const uint8_t *aBuf, uint16_t aBufLength)
-{
-    otError error = OT_ERROR_NONE;
-
-    otEXPECT_ACTION(sTransmitBuffer == NULL, error = OT_ERROR_BUSY);
-
-    sTransmitBuffer = aBuf;
-    sTransmitLength = aBufLength;
-
-exit:
-    return error;
-}
-
-void processReceive(void)
-{
-    // Copy tail to prevent multiple reads
-    uint16_t tail = sReceive.mTail;
-
-    // If the data wraps around, process the first part
-    if (sReceive.mHead > tail)
-    {
-        otPlatUartReceived(sReceive.mBuffer + sReceive.mHead, kReceiveBufferSize - sReceive.mHead);
-
-        // Reset the buffer mHead back to zero.
-        sReceive.mHead = 0;
-    }
-
-    // For any data remaining, process it
-    if (sReceive.mHead != tail)
-    {
-        otPlatUartReceived(sReceive.mBuffer + sReceive.mHead, tail - sReceive.mHead);
-
-        // Set mHead to the local tail we have cached
-        sReceive.mHead = tail;
-    }
-}
-
-otError otPlatUartFlush(void)
-{
-    otEXPECT(sTransmitBuffer != NULL);
-
-    for (; sTransmitLength > 0; sTransmitLength--)
-    {
-        while (HWREG(UART0_BASE + UART_O_FR) & UART_FR_TXFF)
-            ;
-
-        HWREG(UART0_BASE + UART_O_DR) = *sTransmitBuffer++;
-    }
-
-    sTransmitBuffer = NULL;
-    return OT_ERROR_NONE;
-
-exit:
-    return OT_ERROR_INVALID_STATE;
-}
-
-void processTransmit(void)
-{
-    otPlatUartFlush();
-    otPlatUartSendDone();
-}
-
-void cc2538UartProcess(void)
-{
-    processReceive();
-    processTransmit();
-}
-
-void UART0IntHandler(void)
-{
-    uint32_t mis;
-    uint8_t  byte;
-
-    mis                            = HWREG(UART0_BASE + UART_O_MIS);
-    HWREG(UART0_BASE + UART_O_ICR) = mis;
-
-    if (mis & (UART_IM_RXIM | UART_IM_RTIM))
-    {
-        while (!(HWREG(UART0_BASE + UART_O_FR) & UART_FR_RXFE))
-        {
-            byte = HWREG(UART0_BASE + UART_O_DR);
-
-            // We can only write if incrementing mTail doesn't equal mHead
-            if (sReceive.mHead != (sReceive.mTail + 1) % kReceiveBufferSize)
-            {
-                sReceive.mBuffer[sReceive.mTail] = byte;
-                sReceive.mTail                   = (sReceive.mTail + 1) % kReceiveBufferSize;
-            }
-        }
-    }
-}
-
-#if OPENTHREAD_CONFIG_ENABLE_DEBUG_UART
-
-int otPlatDebugUart_kbhit(void)
-{
-    uint32_t v;
-
-    /* get flags */
-    v = HWREG(UART1_BASE + UART_O_FR);
-
-    /* if FIFO empty we have no data */
-    return !(v & UART_FR_RXFE);
-}
-
-int otPlatDebugUart_getc(void)
-{
-    int v = 1;
-
-    /* if nothing in fifo */
-    if (!otPlatDebugUart_kbhit())
-    {
-        return -1;
-    }
-
-    /* fetch */
-    v = (int)HWREG(UART0_BASE + UART_O_DR);
-    v = (v & 0x0ff);
-    return v;
-}
-
-void otPlatDebugUart_putchar_raw(int b)
-{
-    /* wait till not busy */
-    while (HWREG(UART1_BASE + UART_O_FR) & UART_FR_TXFF)
-        ;
-
-    /* write byte */
-    HWREG(UART1_BASE + UART_O_DR) = ((uint32_t)(b & 0x0ff));
-}
-
-void cc2538DebugUartInit(void)
-{
-    int32_t a, b;
-
-    // clocks
-    enable_uart_clocks();
-
-    HWREG(UART1_BASE + UART_O_CC) = 0;
-
-    // UART1 - tx pin
-    // Using an RF06 Evaluation board
-    // http://www.ti.com/tool/cc2538dk
-    // PA3 => is jumper position RF1.14
-    // To use these, you will require a "flying-lead" UART adapter
-    HWREG(IOC_PA3_SEL)  = IOC_MUX_OUT_SEL_UART1_TXD;
-    HWREG(IOC_PA3_OVER) = IOC_OVERRIDE_OE;
-    HWREG(GPIO_A_BASE + GPIO_O_AFSEL) |= GPIO_PIN_3;
-
-    // UART1 - rx pin we don't really use but we setup anyway
-    // PA2 => is jumper position RF1.16
-    HWREG(IOC_UARTRXD_UART1) = IOC_PAD_IN_SEL_PA2;
-    HWREG(IOC_PA2_OVER)      = IOC_OVERRIDE_DIS;
-    HWREG(GPIO_A_BASE + GPIO_O_AFSEL) |= GPIO_PIN_2;
-
-    HWREG(UART1_BASE + UART_O_CC) = 0;
-
-    // baud rate
-    b = (((kPlatformClock * 8) / kBaudRate) + 1) / 2;
-    a = b / 64;
-    b = b % 64;
-
-    HWREG(UART1_BASE + UART_O_IBRD) = a;
-    HWREG(UART1_BASE + UART_O_FBRD) = b;
-    HWREG(UART1_BASE + UART_O_LCRH) = UART_CONFIG_WLEN_8 | UART_CONFIG_STOP_ONE | UART_CONFIG_PAR_NONE;
-
-    /* NOTE:
-     *  uart1 is not using IRQs it is tx only
-     *  and we block when writing bytes
-     */
-    HWREG(UART1_BASE + UART_O_CTL) = UART_CTL_UARTEN | UART_CTL_TXE | UART_CTL_RXE;
-}
-
-#endif
diff --git a/examples/platforms/simulation/alarm.c b/examples/platforms/simulation/alarm.c
index 2795672..d826d11 100644
--- a/examples/platforms/simulation/alarm.c
+++ b/examples/platforms/simulation/alarm.c
@@ -162,10 +162,7 @@
 }
 #endif // defined(CLOCK_MONOTONIC_RAW) || defined(CLOCK_MONOTONIC)
 
-uint32_t otPlatAlarmMilliGetNow(void)
-{
-    return (uint32_t)(platformGetNow() / US_PER_MS);
-}
+uint32_t otPlatAlarmMilliGetNow(void) { return (uint32_t)(platformGetNow() / US_PER_MS); }
 
 void otPlatAlarmMilliStartAt(otInstance *aInstance, uint32_t aT0, uint32_t aDt)
 {
@@ -182,10 +179,7 @@
     sIsMsRunning = false;
 }
 
-uint32_t otPlatAlarmMicroGetNow(void)
-{
-    return (uint32_t)platformGetNow();
-}
+uint32_t otPlatAlarmMicroGetNow(void) { return (uint32_t)platformGetNow(); }
 
 void otPlatAlarmMicroStartAt(otInstance *aInstance, uint32_t aT0, uint32_t aDt)
 {
@@ -303,14 +297,8 @@
 #endif
 }
 
-uint64_t otPlatTimeGet(void)
-{
-    return platformGetNow();
-}
+uint64_t otPlatTimeGet(void) { return platformGetNow(); }
 
-uint16_t otPlatTimeGetXtalAccuracy(void)
-{
-    return 0;
-}
+uint16_t otPlatTimeGetXtalAccuracy(void) { return 0; }
 
 #endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
diff --git a/examples/platforms/simulation/crypto.c b/examples/platforms/simulation/crypto.c
index 36cf89b..b220f7c 100644
--- a/examples/platforms/simulation/crypto.c
+++ b/examples/platforms/simulation/crypto.c
@@ -38,12 +38,12 @@
 
 // crypto key storage stubs
 
-otError otPlatCryptoImportKey(otCryptoKeyRef *     aKeyRef,
+otError otPlatCryptoImportKey(otCryptoKeyRef      *aKeyRef,
                               otCryptoKeyType      aKeyType,
                               otCryptoKeyAlgorithm aKeyAlgorithm,
                               int                  aKeyUsage,
                               otCryptoKeyStorage   aKeyPersistence,
-                              const uint8_t *      aKey,
+                              const uint8_t       *aKey,
                               size_t               aKeyLen)
 {
     OT_UNUSED_VARIABLE(aKeyRef);
diff --git a/examples/platforms/simulation/diag.c b/examples/platforms/simulation/diag.c
index ddb8c26..dcd9f1d 100644
--- a/examples/platforms/simulation/diag.c
+++ b/examples/platforms/simulation/diag.c
@@ -35,8 +35,11 @@
 
 #include <openthread/config.h>
 #include <openthread/platform/alarm-milli.h>
+#include <openthread/platform/diag.h>
 #include <openthread/platform/radio.h>
 
+#include "utils/code_utils.h"
+
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
 
 /**
@@ -45,25 +48,23 @@
  */
 static bool sDiagMode = false;
 
-void otPlatDiagModeSet(bool aMode)
+enum
 {
-    sDiagMode = aMode;
-}
+    SIM_GPIO = 0,
+};
 
-bool otPlatDiagModeGet()
-{
-    return sDiagMode;
-}
+static otGpioMode sGpioMode  = OT_GPIO_MODE_INPUT;
+static bool       sGpioValue = false;
+static uint8_t    sRawPowerSetting[OPENTHREAD_CONFIG_POWER_CALIBRATION_RAW_POWER_SETTING_SIZE];
+static uint16_t   sRawPowerSettingLength = 0;
 
-void otPlatDiagChannelSet(uint8_t aChannel)
-{
-    OT_UNUSED_VARIABLE(aChannel);
-}
+void otPlatDiagModeSet(bool aMode) { sDiagMode = aMode; }
 
-void otPlatDiagTxPowerSet(int8_t aTxPower)
-{
-    OT_UNUSED_VARIABLE(aTxPower);
-}
+bool otPlatDiagModeGet(void) { return sDiagMode; }
+
+void otPlatDiagChannelSet(uint8_t aChannel) { OT_UNUSED_VARIABLE(aChannel); }
+
+void otPlatDiagTxPowerSet(int8_t aTxPower) { OT_UNUSED_VARIABLE(aTxPower); }
 
 void otPlatDiagRadioReceived(otInstance *aInstance, otRadioFrame *aFrame, otError aError)
 {
@@ -72,9 +73,107 @@
     OT_UNUSED_VARIABLE(aError);
 }
 
-void otPlatDiagAlarmCallback(otInstance *aInstance)
+void otPlatDiagAlarmCallback(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
+
+otError otPlatDiagGpioSet(uint32_t aGpio, bool aValue)
 {
-    OT_UNUSED_VARIABLE(aInstance);
+    otError error = OT_ERROR_NONE;
+
+    otEXPECT_ACTION(aGpio == SIM_GPIO, error = OT_ERROR_INVALID_ARGS);
+    sGpioValue = aValue;
+
+exit:
+    return error;
 }
 
+otError otPlatDiagGpioGet(uint32_t aGpio, bool *aValue)
+{
+    otError error = OT_ERROR_NONE;
+
+    otEXPECT_ACTION((aGpio == SIM_GPIO) && (aValue != NULL), error = OT_ERROR_INVALID_ARGS);
+    *aValue = sGpioValue;
+
+exit:
+    return error;
+}
+
+otError otPlatDiagGpioSetMode(uint32_t aGpio, otGpioMode aMode)
+{
+    otError error = OT_ERROR_NONE;
+
+    otEXPECT_ACTION(aGpio == SIM_GPIO, error = OT_ERROR_INVALID_ARGS);
+    sGpioMode = aMode;
+
+exit:
+    return error;
+}
+
+otError otPlatDiagGpioGetMode(uint32_t aGpio, otGpioMode *aMode)
+{
+    otError error = OT_ERROR_NONE;
+
+    otEXPECT_ACTION((aGpio == SIM_GPIO) && (aMode != NULL), error = OT_ERROR_INVALID_ARGS);
+    *aMode = sGpioMode;
+
+exit:
+    return error;
+}
+
+otError otPlatDiagRadioSetRawPowerSetting(otInstance    *aInstance,
+                                          const uint8_t *aRawPowerSetting,
+                                          uint16_t       aRawPowerSettingLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    otError error = OT_ERROR_NONE;
+
+    otEXPECT_ACTION((aRawPowerSetting != NULL) && (aRawPowerSettingLength <= sizeof(sRawPowerSetting)),
+                    error = OT_ERROR_INVALID_ARGS);
+    memcpy(sRawPowerSetting, aRawPowerSetting, aRawPowerSettingLength);
+    sRawPowerSettingLength = aRawPowerSettingLength;
+
+exit:
+    return error;
+}
+
+otError otPlatDiagRadioGetRawPowerSetting(otInstance *aInstance,
+                                          uint8_t    *aRawPowerSetting,
+                                          uint16_t   *aRawPowerSettingLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    otError error = OT_ERROR_NONE;
+
+    otEXPECT_ACTION((aRawPowerSetting != NULL) && (aRawPowerSettingLength != NULL), error = OT_ERROR_INVALID_ARGS);
+    otEXPECT_ACTION((sRawPowerSettingLength != 0), error = OT_ERROR_NOT_FOUND);
+    otEXPECT_ACTION((sRawPowerSettingLength <= *aRawPowerSettingLength), error = OT_ERROR_INVALID_ARGS);
+
+    memcpy(aRawPowerSetting, sRawPowerSetting, sRawPowerSettingLength);
+    *aRawPowerSettingLength = sRawPowerSettingLength;
+
+exit:
+    return error;
+}
+
+otError otPlatDiagRadioRawPowerSettingEnable(otInstance *aInstance, bool aEnable)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aEnable);
+
+    return OT_ERROR_NONE;
+}
+
+otError otPlatDiagRadioTransmitCarrier(otInstance *aInstance, bool aEnable)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aEnable);
+
+    return OT_ERROR_NONE;
+}
+
+otError otPlatDiagRadioTransmitStream(otInstance *aInstance, bool aEnable)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aEnable);
+
+    return OT_ERROR_NONE;
+}
 #endif // OPENTHREAD_CONFIG_DIAG_ENABLE
diff --git a/examples/platforms/simulation/entropy.c b/examples/platforms/simulation/entropy.c
index bcb04a4..b567b0b 100644
--- a/examples/platforms/simulation/entropy.c
+++ b/examples/platforms/simulation/entropy.c
@@ -93,7 +93,7 @@
 
 #if __SANITIZE_ADDRESS__ == 0
 
-    FILE * file = NULL;
+    FILE  *file = NULL;
     size_t readLength;
 
     otEXPECT_ACTION(aOutput && aOutputLength, error = OT_ERROR_INVALID_ARGS);
diff --git a/examples/platforms/simulation/infra_if.c b/examples/platforms/simulation/infra_if.c
index b62041b..1904d75 100644
--- a/examples/platforms/simulation/infra_if.c
+++ b/examples/platforms/simulation/infra_if.c
@@ -41,7 +41,7 @@
 
 otError otPlatInfraIfSendIcmp6Nd(uint32_t            aInfraIfIndex,
                                  const otIp6Address *aDestAddress,
-                                 const uint8_t *     aBuffer,
+                                 const uint8_t      *aBuffer,
                                  uint16_t            aBufferLength)
 {
     OT_UNUSED_VARIABLE(aInfraIfIndex);
@@ -51,4 +51,11 @@
 
     return OT_ERROR_FAILED;
 }
+
+otError otPlatInfraIfDiscoverNat64Prefix(uint32_t aInfraIfIndex)
+{
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+
+    return OT_ERROR_FAILED;
+}
 #endif
diff --git a/examples/platforms/simulation/logging.c b/examples/platforms/simulation/logging.c
index 8b90c43..cce2ad5 100644
--- a/examples/platforms/simulation/logging.c
+++ b/examples/platforms/simulation/logging.c
@@ -31,6 +31,7 @@
 #include <openthread/config.h>
 
 #include <ctype.h>
+#include <errno.h>
 #include <inttypes.h>
 #include <stdarg.h>
 #include <stdint.h>
@@ -44,21 +45,82 @@
 #include "utils/code_utils.h"
 
 #if (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED)
-OT_TOOL_WEAK void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, ...)
+
+static FILE *sLogFile = NULL;
+
+void platformLoggingSetFileName(const char *aName)
+{
+    if (sLogFile != NULL)
+    {
+        fclose(sLogFile);
+    }
+
+    sLogFile = fopen(aName, "wt");
+
+    if (sLogFile == NULL)
+    {
+        fprintf(stderr, "Failed to open log file '%s': %s\r\n", aName, strerror(errno));
+        exit(EXIT_FAILURE);
+    }
+}
+
+void platformLoggingInit(const char *aName)
+{
+    if (sLogFile == NULL)
+    {
+        openlog(aName, LOG_PID, LOG_USER);
+        setlogmask(setlogmask(0) & LOG_UPTO(LOG_NOTICE));
+    }
+    else
+    {
+        fprintf(sLogFile, "OpenThread logs\r\n");
+        fprintf(sLogFile, "- Program:  %s\r\n", aName);
+        fprintf(sLogFile, "- Platform: simulation\r\n");
+        fprintf(sLogFile, "- Node ID:  %lu\r\n", (unsigned long)gNodeId);
+        fprintf(sLogFile, "\r\n");
+    }
+}
+
+void platformLoggingDeinit(void)
+{
+    if (sLogFile != NULL)
+    {
+        fclose(sLogFile);
+        sLogFile = NULL;
+    }
+}
+
+void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, ...)
 {
     OT_UNUSED_VARIABLE(aLogLevel);
     OT_UNUSED_VARIABLE(aLogRegion);
 
-    char    logString[512];
-    int     offset;
     va_list args;
 
-    offset = snprintf(logString, sizeof(logString), "[%d]", gNodeId);
-
     va_start(args, aFormat);
-    vsnprintf(&logString[offset], sizeof(logString) - (uint16_t)offset, aFormat, args);
-    va_end(args);
 
-    syslog(LOG_CRIT, "%s", logString);
+    if (sLogFile == NULL)
+    {
+        char logString[512];
+        int  offset;
+
+        offset = snprintf(logString, sizeof(logString), "[%lu]", (unsigned long)gNodeId);
+
+        vsnprintf(&logString[offset], sizeof(logString) - (uint16_t)offset, aFormat, args);
+        syslog(LOG_CRIT, "%s", logString);
+    }
+    else
+    {
+        vfprintf(sLogFile, aFormat, args);
+        fprintf(sLogFile, "\r\n");
+    }
+
+    va_end(args);
 }
-#endif
+
+#else
+
+void platformLoggingInit(const char *aName) { OT_UNUSED_VARIABLE(aName); }
+void platformLoggingDeinit(void) {}
+
+#endif // (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED)
diff --git a/examples/platforms/simulation/openthread-core-simulation-config.h b/examples/platforms/simulation/openthread-core-simulation-config.h
index 879f069..f713830 100644
--- a/examples/platforms/simulation/openthread-core-simulation-config.h
+++ b/examples/platforms/simulation/openthread-core-simulation-config.h
@@ -254,4 +254,35 @@
 #define OPENTHREAD_CONFIG_SRP_CLIENT_BUFFERS_MAX_SERVICES 20
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_DETERMINISTIC_ECDSA_ENABLE
+ *
+ * Define to 1 to generate ECDSA signatures deterministically
+ * according to RFC 6979 instead of randomly.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_DETERMINISTIC_ECDSA_ENABLE
+#define OPENTHREAD_CONFIG_DETERMINISTIC_ECDSA_ENABLE 1
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE
+ *
+ * Define as 1 to enable power calibration support.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE
+#define OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE 1
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+ *
+ * Define as 1 to enable platform power calibration support.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+#define OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE 1
+#endif
+
 #endif // OPENTHREAD_CORE_SIMULATION_CONFIG_H_
diff --git a/examples/platforms/simulation/platform-simulation.h b/examples/platforms/simulation/platform-simulation.h
index 9379421..0fa32fd 100644
--- a/examples/platforms/simulation/platform-simulation.h
+++ b/examples/platforms/simulation/platform-simulation.h
@@ -186,6 +186,28 @@
 void platformRandomInit(void);
 
 /**
+ * This functions set the file name to use for logging.
+ *
+ * @param[in] aName  The file name.
+ *
+ */
+void platformLoggingSetFileName(const char *aName);
+
+/**
+ * This function initializes the platform logging service.
+ *
+ * @param[in] aName    The log module name to set with syslog.
+ *
+ */
+void platformLoggingInit(const char *aName);
+
+/**
+ * This function finalizes the platform logging service.
+ *
+ */
+void platformLoggingDeinit(void);
+
+/**
  * This function updates the file descriptor sets with file descriptors used by the UART driver.
  *
  * @param[in,out]  aReadFdSet   A pointer to the read file descriptors.
@@ -232,6 +254,18 @@
  */
 bool platformRadioIsTransmitPending(void);
 
+/**
+ * This function parses an environment variable as an unsigned 16-bit integer.
+ *
+ * If the environment variable does not exist, this function does nothing.
+ * If it is not a valid integer, this function will terminate the process with an error message.
+ *
+ * @param[in]   aEnvName  The name of the environment variable.
+ * @param[out]  aValue    A pointer to the unsigned 16-bit integer.
+ *
+ */
+void parseFromEnvAsUint16(const char *aEnvName, uint16_t *aValue);
+
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
 
 /**
diff --git a/examples/platforms/simulation/radio.c b/examples/platforms/simulation/radio.c
index 2b627c2..9fe37b6 100644
--- a/examples/platforms/simulation/radio.c
+++ b/examples/platforms/simulation/radio.c
@@ -31,6 +31,7 @@
 #include <errno.h>
 #include <sys/time.h>
 
+#include <openthread/cli.h>
 #include <openthread/dataset.h>
 #include <openthread/link.h>
 #include <openthread/random_noncrypto.h>
@@ -71,10 +72,12 @@
 
 #if OPENTHREAD_SIMULATION_VIRTUAL_TIME
 extern int      sSockFd;
+extern uint16_t sPortBase;
 extern uint16_t sPortOffset;
 #else
 static int      sTxFd       = -1;
 static int      sRxFd       = -1;
+static uint16_t sPortBase   = 9000;
 static uint16_t sPortOffset = 0;
 static uint16_t sPort       = 0;
 #endif
@@ -166,11 +169,128 @@
 
 static int8_t GetRssi(uint16_t aChannel);
 
-static bool IsTimeAfterOrEqual(uint32_t aTimeA, uint32_t aTimeB)
+#if OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
+
+static enum {
+    kFilterOff,
+    kFilterDenyList,
+    kFilterAllowList,
+} sFilterMode = kFilterOff;
+
+static uint8_t sFilterNodeIdsBitVector[(MAX_NETWORK_SIZE + 7) / 8];
+
+static bool FilterContainsId(uint16_t aNodeId)
 {
-    return (aTimeA - aTimeB) < (1U << 31);
+    uint16_t index = aNodeId - 1;
+
+    return (sFilterNodeIdsBitVector[index / 8] & (0x80 >> (index % 8))) != 0;
 }
 
+static bool NodeIdFilterIsConnectable(uint16_t aNodeId)
+{
+    bool isConnectable = true;
+
+    switch (sFilterMode)
+    {
+    case kFilterOff:
+        break;
+    case kFilterDenyList:
+        isConnectable = !FilterContainsId(aNodeId);
+        break;
+    case kFilterAllowList:
+        isConnectable = FilterContainsId(aNodeId);
+        break;
+    }
+
+    return isConnectable;
+}
+
+static void AddNodeIdToFilter(uint16_t aNodeId)
+{
+    uint16_t index = aNodeId - 1;
+
+    sFilterNodeIdsBitVector[index / 8] |= 0x80 >> (index % 8);
+}
+
+OT_TOOL_WEAK void otCliOutputFormat(const char *aFmt, ...) { OT_UNUSED_VARIABLE(aFmt); }
+
+otError ProcessNodeIdFilter(void *aContext, uint8_t aArgsLength, char *aArgs[])
+{
+    OT_UNUSED_VARIABLE(aContext);
+
+    otError error = OT_ERROR_NONE;
+    bool    deny  = false;
+
+    if (aArgsLength == 0)
+    {
+        switch (sFilterMode)
+        {
+        case kFilterOff:
+            otCliOutputFormat("off");
+            break;
+        case kFilterDenyList:
+            otCliOutputFormat("deny-list");
+            break;
+        case kFilterAllowList:
+            otCliOutputFormat("allow-list");
+            break;
+        }
+
+        for (uint16_t nodeId = 0; nodeId <= MAX_NETWORK_SIZE; nodeId++)
+        {
+            if (FilterContainsId(nodeId))
+            {
+                otCliOutputFormat(" %d", nodeId);
+            }
+        }
+
+        otCliOutputFormat("\r\n");
+    }
+    else if (!strcmp(aArgs[0], "clear"))
+    {
+        otEXPECT_ACTION(aArgsLength == 1, error = OT_ERROR_INVALID_ARGS);
+
+        memset(sFilterNodeIdsBitVector, 0, sizeof(sFilterNodeIdsBitVector));
+        sFilterMode = kFilterOff;
+    }
+    else if ((deny = !strcmp(aArgs[0], "deny")) || !strcmp(aArgs[0], "allow"))
+    {
+        uint16_t nodeId;
+        char    *endptr;
+
+        otEXPECT_ACTION(aArgsLength == 2, error = OT_ERROR_INVALID_ARGS);
+
+        nodeId = (uint16_t)strtol(aArgs[1], &endptr, 0);
+
+        otEXPECT_ACTION(*endptr == '\0', error = OT_ERROR_INVALID_ARGS);
+        otEXPECT_ACTION(1 <= nodeId && nodeId <= MAX_NETWORK_SIZE, error = OT_ERROR_INVALID_ARGS);
+
+        otEXPECT_ACTION(sFilterMode != (deny ? kFilterAllowList : kFilterDenyList), error = OT_ERROR_INVALID_STATE);
+
+        AddNodeIdToFilter(nodeId);
+        sFilterMode = deny ? kFilterDenyList : kFilterAllowList;
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_COMMAND;
+    }
+
+exit:
+    return error;
+}
+#else
+otError ProcessNodeIdFilter(void *aContext, uint8_t aArgsLength, char *aArgs[])
+{
+    OT_UNUSED_VARIABLE(aContext);
+    OT_UNUSED_VARIABLE(aArgsLength);
+    OT_UNUSED_VARIABLE(aArgs);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+#endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
+
+static bool IsTimeAfterOrEqual(uint32_t aTimeA, uint32_t aTimeB) { return (aTimeA - aTimeB) < (1U << 31); }
+
 static void ReverseExtAddress(otExtAddress *aReversed, const otExtAddress *aOrigin)
 {
     for (size_t i = 0; i < sizeof(*aReversed); i++)
@@ -298,7 +418,7 @@
 
     otEXPECT_ACTION((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) != -1, perror("socket(sTxFd)"));
 
-    sPort                    = (uint16_t)(9000 + sPortOffset + gNodeId);
+    sPort                    = (uint16_t)(sPortBase + sPortOffset + gNodeId);
     sockaddr.sin_family      = AF_INET;
     sockaddr.sin_port        = htons(sPort);
     sockaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
@@ -337,7 +457,7 @@
     }
 
     sockaddr.sin_family      = AF_INET;
-    sockaddr.sin_port        = htons((uint16_t)(9000 + sPortOffset));
+    sockaddr.sin_port        = htons((uint16_t)(sPortBase + sPortOffset));
     sockaddr.sin_addr.s_addr = inet_addr(OT_RADIO_GROUP);
 
     otEXPECT_ACTION(bind(fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != -1, perror("bind(sRxFd)"));
@@ -356,24 +476,10 @@
 void platformRadioInit(void)
 {
 #if OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
-    char *offset;
+    parseFromEnvAsUint16("PORT_BASE", &sPortBase);
 
-    offset = getenv("PORT_OFFSET");
-
-    if (offset)
-    {
-        char *endptr;
-
-        sPortOffset = (uint16_t)strtol(offset, &endptr, 0);
-
-        if (*endptr != '\0')
-        {
-            fprintf(stderr, "Invalid PORT_OFFSET: %s\n", offset);
-            exit(EXIT_FAILURE);
-        }
-
-        sPortOffset *= (MAX_NETWORK_SIZE + 1);
-    }
+    parseFromEnvAsUint16("PORT_OFFSET", &sPortOffset);
+    sPortOffset *= (MAX_NETWORK_SIZE + 1);
 
     initFds();
 #endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
@@ -737,10 +843,7 @@
     return;
 }
 
-bool platformRadioIsTransmitPending(void)
-{
-    return sState == OT_RADIO_STATE_TRANSMIT && !sTxWait;
-}
+bool platformRadioIsTransmitPending(void) { return sState == OT_RADIO_STATE_TRANSMIT && !sTxWait; }
 
 #if OPENTHREAD_SIMULATION_VIRTUAL_TIME
 void platformRadioReceive(otInstance *aInstance, uint8_t *aBuf, uint16_t aBufLength)
@@ -829,7 +932,10 @@
 
         if (rval > 0)
         {
-            if (sockaddr.sin_port != htons(sPort))
+            uint16_t srcPort   = ntohs(sockaddr.sin_port);
+            uint16_t srcNodeId = srcPort - sPortOffset - sPortBase;
+
+            if (NodeIdFilterIsConnectable(srcNodeId) && srcPort != sPort)
             {
                 sReceiveFrame.mLength = (uint16_t)(rval - 1);
 
@@ -847,7 +953,7 @@
             exit(EXIT_FAILURE);
         }
     }
-#endif
+#endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME == 0
     if (platformRadioIsTransmitPending())
     {
         radioSendMessage(aInstance);
@@ -870,7 +976,7 @@
     sockaddr.sin_family = AF_INET;
     inet_pton(AF_INET, OT_RADIO_GROUP, &sockaddr.sin_addr);
 
-    sockaddr.sin_port = htons((uint16_t)(9000 + sPortOffset));
+    sockaddr.sin_port = htons((uint16_t)(sPortBase + sPortOffset));
     rval =
         sendto(sTxFd, (const char *)aMessage, 1 + aFrame->mLength, 0, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
 
@@ -1216,7 +1322,7 @@
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-otError otPlatRadioEnableCsl(otInstance *        aInstance,
+otError otPlatRadioEnableCsl(otInstance         *aInstance,
                              uint32_t            aCslPeriod,
                              otShortAddress      aShortAddr,
                              const otExtAddress *aExtAddr)
@@ -1247,7 +1353,7 @@
 }
 #endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 
-void otPlatRadioSetMacKey(otInstance *            aInstance,
+void otPlatRadioSetMacKey(otInstance             *aInstance,
                           uint8_t                 aKeyIdMode,
                           uint8_t                 aKeyId,
                           const otMacKeyMaterial *aPrevKey,
@@ -1291,10 +1397,10 @@
 }
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-otError otPlatRadioConfigureEnhAckProbing(otInstance *         aInstance,
+otError otPlatRadioConfigureEnhAckProbing(otInstance          *aInstance,
                                           otLinkMetrics        aLinkMetrics,
                                           const otShortAddress aShortAddress,
-                                          const otExtAddress * aExtAddress)
+                                          const otExtAddress  *aExtAddress)
 {
     OT_UNUSED_VARIABLE(aInstance);
 
@@ -1321,3 +1427,21 @@
 exit:
     return error;
 }
+
+void parseFromEnvAsUint16(const char *aEnvName, uint16_t *aValue)
+{
+    char *env = getenv(aEnvName);
+
+    if (env)
+    {
+        char *endptr;
+
+        *aValue = (uint16_t)strtol(env, &endptr, 0);
+
+        if (*endptr != '\0')
+        {
+            fprintf(stderr, "Invalid %s: %s\n", aEnvName, env);
+            exit(EXIT_FAILURE);
+        }
+    }
+}
diff --git a/examples/platforms/simulation/spi-stubs.c b/examples/platforms/simulation/spi-stubs.c
index 585bca7..2643402 100644
--- a/examples/platforms/simulation/spi-stubs.c
+++ b/examples/platforms/simulation/spi-stubs.c
@@ -40,7 +40,7 @@
 
 otError otPlatSpiSlaveEnable(otPlatSpiSlaveTransactionCompleteCallback aCompleteCallback,
                              otPlatSpiSlaveTransactionProcessCallback  aProcessCallback,
-                             void *                                    aContext)
+                             void                                     *aContext)
 {
     OT_UNUSED_VARIABLE(aCompleteCallback);
     OT_UNUSED_VARIABLE(aProcessCallback);
@@ -52,9 +52,7 @@
     return OT_ERROR_NOT_IMPLEMENTED;
 }
 
-void otPlatSpiSlaveDisable(void)
-{
-}
+void otPlatSpiSlaveDisable(void) {}
 
 otError otPlatSpiSlavePrepareTransaction(uint8_t *aOutputBuf,
                                          uint16_t aOutputBufLen,
@@ -73,9 +71,7 @@
 
 // Uart
 
-void otPlatUartSendDone(void)
-{
-}
+void otPlatUartSendDone(void) {}
 
 void otPlatUartReceived(const uint8_t *aBuf, uint16_t aBufLength)
 {
diff --git a/examples/platforms/simulation/system.c b/examples/platforms/simulation/system.c
index f9282a6..0e86aaa 100644
--- a/examples/platforms/simulation/system.c
+++ b/examples/platforms/simulation/system.c
@@ -44,7 +44,6 @@
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
-#include <syslog.h>
 
 #include <openthread/tasklet.h>
 #include <openthread/platform/alarm-milli.h>
@@ -74,6 +73,7 @@
     OT_SIM_OPT_ENABLE_ENERGY_SCAN = 'E',
     OT_SIM_OPT_SLEEP_TO_TX        = 't',
     OT_SIM_OPT_TIME_SPEED         = 's',
+    OT_SIM_OPT_LOG_FILE           = 'l',
     OT_SIM_OPT_UNKNOWN            = '?',
 };
 
@@ -86,7 +86,11 @@
             "    -h --help                  Display this usage information.\n"
             "    -E --enable-energy-scan    Enable energy scan capability.\n"
             "    -t --sleep-to-tx           Let radio support direct transition from sleep to TX with CSMA.\n"
-            "    -s --time-speed=val        Speed up the time in simulation.\n",
+            "    -s --time-speed=val        Speed up the time in simulation.\n"
+#if (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED)
+            "    -l --log-file=name         File name to write logs.\n"
+#endif
+            ,
             aProgramName);
 
     exit(aExitCode);
@@ -94,17 +98,26 @@
 
 void otSysInit(int aArgCount, char *aArgVector[])
 {
-    char *   endptr;
+    char    *endptr;
     uint32_t speedUpFactor = 1;
 
     static const struct option long_options[] = {
         {"help", no_argument, 0, OT_SIM_OPT_HELP},
-        {"enable-energy-scan", no_argument, 0, OT_SIM_OPT_SLEEP_TO_TX},
+        {"enable-energy-scan", no_argument, 0, OT_SIM_OPT_ENABLE_ENERGY_SCAN},
         {"sleep-to-tx", no_argument, 0, OT_SIM_OPT_SLEEP_TO_TX},
         {"time-speed", required_argument, 0, OT_SIM_OPT_TIME_SPEED},
+#if (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED)
+        {"log-file", required_argument, 0, OT_SIM_OPT_LOG_FILE},
+#endif
         {0, 0, 0, 0},
     };
 
+#if (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED)
+    static const char options[] = "Ehts:l:";
+#else
+    static const char options[] = "Ehts:";
+#endif
+
     if (gPlatformPseudoResetWasRequested)
     {
         gPlatformPseudoResetWasRequested = false;
@@ -115,7 +128,7 @@
 
     while (true)
     {
-        int c = getopt_long(aArgCount, aArgVector, "Ehts:", long_options, NULL);
+        int c = getopt_long(aArgCount, aArgVector, options, long_options, NULL);
 
         if (c == -1)
         {
@@ -144,6 +157,11 @@
                 exit(EXIT_FAILURE);
             }
             break;
+#if OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED
+        case OT_SIM_OPT_LOG_FILE:
+            platformLoggingSetFileName(optarg);
+            break;
+#endif
         default:
             break;
         }
@@ -162,12 +180,10 @@
         exit(EXIT_FAILURE);
     }
 
-    openlog(basename(aArgVector[0]), LOG_PID, LOG_USER);
-    setlogmask(setlogmask(0) & LOG_UPTO(LOG_NOTICE));
-
     signal(SIGTERM, &handleSignal);
     signal(SIGHUP, &handleSignal);
 
+    platformLoggingInit(basename(aArgVector[0]));
     platformAlarmInit(speedUpFactor);
     platformRadioInit();
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
@@ -176,10 +192,7 @@
     platformRandomInit();
 }
 
-bool otSysPseudoResetWasRequested(void)
-{
-    return gPlatformPseudoResetWasRequested;
-}
+bool otSysPseudoResetWasRequested(void) { return gPlatformPseudoResetWasRequested; }
 
 void otSysDeinit(void)
 {
@@ -187,6 +200,7 @@
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
     platformTrelDeinit();
 #endif
+    platformLoggingDeinit();
 }
 
 void otSysProcessDrivers(otInstance *aInstance)
diff --git a/examples/platforms/simulation/trel.c b/examples/platforms/simulation/trel.c
index 20c6b30..3413c93 100644
--- a/examples/platforms/simulation/trel.c
+++ b/examples/platforms/simulation/trel.c
@@ -366,8 +366,8 @@
 #endif
 }
 
-void otPlatTrelSend(otInstance *      aInstance,
-                    const uint8_t *   aUdpPayload,
+void otPlatTrelSend(otInstance       *aInstance,
+                    const uint8_t    *aUdpPayload,
                     uint16_t          aUdpPayloadLen,
                     const otSockAddr *aDestSockAddr)
 {
@@ -419,10 +419,7 @@
     OT_UNUSED_VARIABLE(aSpeedUpFactor);
 }
 
-void platformTrelDeinit(void)
-{
-    deinitFds();
-}
+void platformTrelDeinit(void) { deinitFds(); }
 
 void platformTrelUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, struct timeval *aTimeout, int *aMaxFd)
 {
diff --git a/examples/platforms/simulation/uart.c b/examples/platforms/simulation/uart.c
index 3bbcfac..fe3fceb 100644
--- a/examples/platforms/simulation/uart.c
+++ b/examples/platforms/simulation/uart.c
@@ -54,15 +54,9 @@
 static struct termios original_stdin_termios;
 static struct termios original_stdout_termios;
 
-static void restore_stdin_termios(void)
-{
-    tcsetattr(s_in_fd, TCSAFLUSH, &original_stdin_termios);
-}
+static void restore_stdin_termios(void) { tcsetattr(s_in_fd, TCSAFLUSH, &original_stdin_termios); }
 
-static void restore_stdout_termios(void)
-{
-    tcsetattr(s_out_fd, TCSAFLUSH, &original_stdout_termios);
-}
+static void restore_stdout_termios(void) { tcsetattr(s_out_fd, TCSAFLUSH, &original_stdout_termios); }
 
 void platformUartRestore(void)
 {
@@ -241,8 +235,8 @@
     ssize_t       rval;
     const int     error_flags = POLLERR | POLLNVAL | POLLHUP;
     struct pollfd pollfd[]    = {
-        {s_in_fd, POLLIN | error_flags, 0},
-        {s_out_fd, POLLOUT | error_flags, 0},
+           {s_in_fd, POLLIN | error_flags, 0},
+           {s_out_fd, POLLOUT | error_flags, 0},
     };
 
     errno = 0;
diff --git a/examples/platforms/simulation/virtual_time/alarm-sim.c b/examples/platforms/simulation/virtual_time/alarm-sim.c
index 10ebaa5..7c2cea1 100644
--- a/examples/platforms/simulation/virtual_time/alarm-sim.c
+++ b/examples/platforms/simulation/virtual_time/alarm-sim.c
@@ -55,20 +55,11 @@
     sNow = 0;
 }
 
-uint64_t platformAlarmGetNow(void)
-{
-    return sNow;
-}
+uint64_t platformAlarmGetNow(void) { return sNow; }
 
-void platformAlarmAdvanceNow(uint64_t aDelta)
-{
-    sNow += aDelta;
-}
+void platformAlarmAdvanceNow(uint64_t aDelta) { sNow += aDelta; }
 
-uint32_t otPlatAlarmMilliGetNow(void)
-{
-    return (uint32_t)(sNow / US_PER_MS);
-}
+uint32_t otPlatAlarmMilliGetNow(void) { return (uint32_t)(sNow / US_PER_MS); }
 
 void otPlatAlarmMilliStartAt(otInstance *aInstance, uint32_t aT0, uint32_t aDt)
 {
@@ -85,10 +76,7 @@
     sIsMsRunning = false;
 }
 
-uint32_t otPlatAlarmMicroGetNow(void)
-{
-    return (uint32_t)sNow;
-}
+uint32_t otPlatAlarmMicroGetNow(void) { return (uint32_t)sNow; }
 
 void otPlatAlarmMicroStartAt(otInstance *aInstance, uint32_t aT0, uint32_t aDt)
 {
@@ -186,16 +174,10 @@
 #endif // OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
 }
 
-uint64_t otPlatTimeGet(void)
-{
-    return platformAlarmGetNow();
-}
+uint64_t otPlatTimeGet(void) { return platformAlarmGetNow(); }
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-uint16_t otPlatTimeGetXtalAccuracy(void)
-{
-    return 0;
-}
+uint16_t otPlatTimeGetXtalAccuracy(void) { return 0; }
 #endif
 
 #endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME
diff --git a/examples/platforms/simulation/virtual_time/platform-sim.c b/examples/platforms/simulation/virtual_time/platform-sim.c
index fa0dd90..1c2c818 100644
--- a/examples/platforms/simulation/virtual_time/platform-sim.c
+++ b/examples/platforms/simulation/virtual_time/platform-sim.c
@@ -61,6 +61,7 @@
 
 uint64_t sNow = 0; // microseconds
 int      sSockFd;
+uint16_t sPortBase = 9000;
 uint16_t sPortOffset;
 
 static void handleSignal(int aSignal)
@@ -78,7 +79,7 @@
     memset(&sockaddr, 0, sizeof(sockaddr));
     sockaddr.sin_family = AF_INET;
     inet_pton(AF_INET, "127.0.0.1", &sockaddr.sin_addr);
-    sockaddr.sin_port = htons(9000 + sPortOffset);
+    sockaddr.sin_port = htons(sPortBase + sPortOffset);
 
     rval = sendto(sSockFd, aEvent, offsetof(struct Event, mData) + aEvent->mDataLength, 0, (struct sockaddr *)&sockaddr,
                   sizeof(sockaddr));
@@ -135,19 +136,11 @@
 }
 
 #if OPENTHREAD_SIMULATION_VIRTUAL_TIME_UART
-void platformUartRestore(void)
-{
-}
+void platformUartRestore(void) {}
 
-otError otPlatUartEnable(void)
-{
-    return OT_ERROR_NONE;
-}
+otError otPlatUartEnable(void) { return OT_ERROR_NONE; }
 
-otError otPlatUartDisable(void)
-{
-    return OT_ERROR_NONE;
-}
+otError otPlatUartDisable(void) { return OT_ERROR_NONE; }
 
 otError otPlatUartSend(const uint8_t *aData, uint16_t aLength)
 {
@@ -167,37 +160,21 @@
     return error;
 }
 
-otError otPlatUartFlush(void)
-{
-    return OT_ERROR_NONE;
-}
+otError otPlatUartFlush(void) { return OT_ERROR_NONE; }
 #endif // OPENTHREAD_SIMULATION_VIRTUAL_TIME_UART
 
 static void socket_init(void)
 {
     struct sockaddr_in sockaddr;
-    char *             offset;
     memset(&sockaddr, 0, sizeof(sockaddr));
     sockaddr.sin_family = AF_INET;
 
-    offset = getenv("PORT_OFFSET");
+    parseFromEnvAsUint16("PORT_BASE", &sPortBase);
 
-    if (offset)
-    {
-        char *endptr;
+    parseFromEnvAsUint16("PORT_OFFSET", &sPortOffset);
+    sPortOffset *= (MAX_NETWORK_SIZE + 1);
 
-        sPortOffset = (uint16_t)strtol(offset, &endptr, 0);
-
-        if (*endptr != '\0')
-        {
-            fprintf(stderr, "Invalid PORT_OFFSET: %s\n", offset);
-            exit(EXIT_FAILURE);
-        }
-
-        sPortOffset *= (MAX_NETWORK_SIZE + 1);
-    }
-
-    sockaddr.sin_port        = htons((uint16_t)(9000 + sPortOffset + gNodeId));
+    sockaddr.sin_port        = htons((uint16_t)(sPortBase + sPortOffset + gNodeId));
     sockaddr.sin_addr.s_addr = INADDR_ANY;
 
     sSockFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
@@ -254,15 +231,9 @@
     signal(SIGHUP, &handleSignal);
 }
 
-bool otSysPseudoResetWasRequested(void)
-{
-    return gPlatformPseudoResetWasRequested;
-}
+bool otSysPseudoResetWasRequested(void) { return gPlatformPseudoResetWasRequested; }
 
-void otSysDeinit(void)
-{
-    close(sSockFd);
-}
+void otSysDeinit(void) { close(sSockFd); }
 
 void otSysProcessDrivers(otInstance *aInstance)
 {
diff --git a/examples/platforms/utils/debug_uart.c b/examples/platforms/utils/debug_uart.c
index e1d10bd..169c65d 100644
--- a/examples/platforms/utils/debug_uart.c
+++ b/examples/platforms/utils/debug_uart.c
@@ -103,22 +103,13 @@
 
 /* provide WEAK stubs for platforms that do not implement all functions */
 OT_TOOL_WEAK
-void otPlatDebugUart_putchar_raw(int c)
-{
-    OT_UNUSED_VARIABLE(c);
-}
+void otPlatDebugUart_putchar_raw(int c) { OT_UNUSED_VARIABLE(c); }
 
 OT_TOOL_WEAK
-int otPlatDebugUart_kbhit(void)
-{
-    return 0; /* nothing */
-}
+int otPlatDebugUart_kbhit(void) { return 0; /* nothing */ }
 
 OT_TOOL_WEAK
-int otPlatDebugUart_getc(void)
-{
-    return -1; /* nothing */
-}
+int otPlatDebugUart_getc(void) { return -1; /* nothing */ }
 
 OT_TOOL_WEAK
 otError otPlatDebugUart_logfile(const char *filename)
diff --git a/examples/platforms/utils/link_metrics.cpp b/examples/platforms/utils/link_metrics.cpp
index a09fab4..84be0d7 100644
--- a/examples/platforms/utils/link_metrics.cpp
+++ b/examples/platforms/utils/link_metrics.cpp
@@ -134,7 +134,7 @@
     otLinkMetrics GetLinkMetrics(void) const { return mLinkMetrics; }
 
 private:
-    uint8_t GetLinkMargin(int8_t aRssi) const { return LinkQualityInfo::ConvertRssToLinkMargin(sNoiseFloor, aRssi); }
+    uint8_t GetLinkMargin(int8_t aRssi) const { return ComputeLinkMargin(sNoiseFloor, aRssi); }
 
     bool Matches(const otShortAddress &aShortAddress) const { return mShortAddress == aShortAddress; };
 
@@ -177,10 +177,7 @@
     return !aLinkMetrics.mPduCount && !aLinkMetrics.mLqi && !aLinkMetrics.mLinkMargin && !aLinkMetrics.mRssi;
 }
 
-void otLinkMetricsInit(int8_t aNoiseFloor)
-{
-    sNoiseFloor = aNoiseFloor;
-}
+void otLinkMetricsInit(int8_t aNoiseFloor) { sNoiseFloor = aNoiseFloor; }
 
 otError otLinkMetricsConfigureEnhAckProbing(otShortAddress      aShortAddress,
                                             const otExtAddress *aExtAddress,
diff --git a/examples/platforms/utils/logging_rtt.c b/examples/platforms/utils/logging_rtt.c
index e58db65..32d79f5 100644
--- a/examples/platforms/utils/logging_rtt.c
+++ b/examples/platforms/utils/logging_rtt.c
@@ -136,10 +136,7 @@
     return;
 }
 
-void utilsLogRttDeinit(void)
-{
-    sLogInitialized = false;
-}
+void utilsLogRttDeinit(void) { sLogInitialized = false; }
 
 void utilsLogRttOutput(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, va_list ap)
 {
diff --git a/examples/platforms/utils/mac_frame.cpp b/examples/platforms/utils/mac_frame.cpp
index de23883..0ae88db 100644
--- a/examples/platforms/utils/mac_frame.cpp
+++ b/examples/platforms/utils/mac_frame.cpp
@@ -68,17 +68,17 @@
 
 bool otMacFrameIsAck(const otRadioFrame *aFrame)
 {
-    return static_cast<const Mac::Frame *>(aFrame)->GetType() == Mac::Frame::kFcfFrameAck;
+    return static_cast<const Mac::Frame *>(aFrame)->GetType() == Mac::Frame::kTypeAck;
 }
 
 bool otMacFrameIsData(const otRadioFrame *aFrame)
 {
-    return static_cast<const Mac::Frame *>(aFrame)->GetType() == Mac::Frame::kFcfFrameData;
+    return static_cast<const Mac::Frame *>(aFrame)->GetType() == Mac::Frame::kTypeData;
 }
 
 bool otMacFrameIsCommand(const otRadioFrame *aFrame)
 {
-    return static_cast<const Mac::Frame *>(aFrame)->GetType() == Mac::Frame::kFcfFrameMacCmd;
+    return static_cast<const Mac::Frame *>(aFrame)->GetType() == Mac::Frame::kTypeMacCmd;
 }
 
 bool otMacFrameIsDataRequest(const otRadioFrame *aFrame)
@@ -164,9 +164,9 @@
 #if OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2
 otError otMacFrameGenerateEnhAck(const otRadioFrame *aFrame,
                                  bool                aIsFramePending,
-                                 const uint8_t *     aIeData,
+                                 const uint8_t      *aIeData,
                                  uint8_t             aIeLength,
-                                 otRadioFrame *      aAckFrame)
+                                 otRadioFrame       *aAckFrame)
 {
     assert(aFrame != nullptr && aAckFrame != nullptr);
 
@@ -206,10 +206,7 @@
     return keyId;
 }
 
-void otMacFrameSetKeyId(otRadioFrame *aFrame, uint8_t aKeyId)
-{
-    static_cast<Mac::Frame *>(aFrame)->SetKeyId(aKeyId);
-}
+void otMacFrameSetKeyId(otRadioFrame *aFrame, uint8_t aKeyId) { static_cast<Mac::Frame *>(aFrame)->SetKeyId(aKeyId); }
 
 uint32_t otMacFrameGetFrameCounter(otRadioFrame *aFrame)
 {
diff --git a/examples/platforms/utils/mac_frame.h b/examples/platforms/utils/mac_frame.h
index 8f0c679..1853ed1 100644
--- a/examples/platforms/utils/mac_frame.h
+++ b/examples/platforms/utils/mac_frame.h
@@ -221,9 +221,9 @@
  */
 otError otMacFrameGenerateEnhAck(const otRadioFrame *aFrame,
                                  bool                aIsFramePending,
-                                 const uint8_t *     aIeData,
+                                 const uint8_t      *aIeData,
                                  uint8_t             aIeLength,
-                                 otRadioFrame *      aAckFrame);
+                                 otRadioFrame       *aAckFrame);
 
 /**
  * Set CSL IE content into the frame.
diff --git a/examples/platforms/utils/otns_utils.cpp b/examples/platforms/utils/otns_utils.cpp
index e343503..1c8db69 100644
--- a/examples/platforms/utils/otns_utils.cpp
+++ b/examples/platforms/utils/otns_utils.cpp
@@ -43,9 +43,6 @@
 #if OPENTHREAD_CONFIG_OTNS_ENABLE
 
 OT_TOOL_WEAK
-void otPlatOtnsStatus(const char *aStatus)
-{
-    LogAlways("[OTNS] %s", aStatus);
-}
+void otPlatOtnsStatus(const char *aStatus) { LogAlways("[OTNS] %s", aStatus); }
 
 #endif // OPENTHREAD_CONFIG_OTNS_ENABLE
diff --git a/examples/platforms/utils/settings_ram.c b/examples/platforms/utils/settings_ram.c
index fd4abd9..d8e7c0f 100644
--- a/examples/platforms/utils/settings_ram.c
+++ b/examples/platforms/utils/settings_ram.c
@@ -66,10 +66,7 @@
     sSettingsBufLength = 0;
 }
 
-void otPlatSettingsDeinit(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
+void otPlatSettingsDeinit(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
 
 otError otPlatSettingsGet(otInstance *aInstance, uint16_t aKey, int aIndex, uint8_t *aValue, uint16_t *aValueLength)
 {
@@ -229,9 +226,6 @@
     return error;
 }
 
-void otPlatSettingsWipe(otInstance *aInstance)
-{
-    otPlatSettingsInit(aInstance, NULL, 0);
-}
+void otPlatSettingsWipe(otInstance *aInstance) { otPlatSettingsInit(aInstance, NULL, 0); }
 
 #endif // OPENTHREAD_SETTINGS_RAM
diff --git a/examples/platforms/utils/soft_source_match_table.c b/examples/platforms/utils/soft_source_match_table.c
index 458d1b5..3d8a8a6 100644
--- a/examples/platforms/utils/soft_source_match_table.c
+++ b/examples/platforms/utils/soft_source_match_table.c
@@ -46,10 +46,7 @@
 #if RADIO_CONFIG_SRC_MATCH_SHORT_ENTRY_NUM || RADIO_CONFIG_SRC_MATCH_EXT_ENTRY_NUM
 static uint16_t sPanId = 0;
 
-void utilsSoftSrcMatchSetPanId(uint16_t aPanId)
-{
-    sPanId = aPanId;
-}
+void utilsSoftSrcMatchSetPanId(uint16_t aPanId) { sPanId = aPanId; }
 #endif // RADIO_CONFIG_SRC_MATCH_SHORT_ENTRY_NUM || RADIO_CONFIG_SRC_MATCH_EXT_ENTRY_NUM
 
 #if RADIO_CONFIG_SRC_MATCH_SHORT_ENTRY_NUM
@@ -148,7 +145,7 @@
 {
     OT_UNUSED_VARIABLE(aInstance);
 
-    otLogDebgPlat("Clear ShortAddr entries", NULL);
+    otLogDebgPlat("Clear ShortAddr entries");
 
     memset(srcMatchShortEntry, 0, sizeof(srcMatchShortEntry));
 }
@@ -260,7 +257,7 @@
 {
     OT_UNUSED_VARIABLE(aInstance);
 
-    otLogDebgPlat("Clear ExtAddr entries", NULL);
+    otLogDebgPlat("Clear ExtAddr entries");
 
     memset(srcMatchExtEntry, 0, sizeof(srcMatchExtEntry));
 }
diff --git a/.lgtm.yml b/examples/platforms/zephyr/CMakeLists.txt
similarity index 85%
copy from .lgtm.yml
copy to examples/platforms/zephyr/CMakeLists.txt
index 9051e95..dcdad63 100644
--- a/.lgtm.yml
+++ b/examples/platforms/zephyr/CMakeLists.txt
@@ -1,5 +1,5 @@
 #
-#  Copyright (c) 2020, The OpenThread Authors.
+#  Copyright (c) 2022, The OpenThread Authors.
 #  All rights reserved.
 #
 #  Redistribution and use in source and binary forms, with or without
@@ -26,12 +26,5 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-extraction:
-  cpp:
-    prepare:
-      packages:
-        - cmake
-        - ninja-build
-    index:
-      build_command:
-        - THREAD_VERSION=1.2 NODE_MODE=rcp OT_NATIVE_IP=1 ./script/test build
+# Intentionally empty, the file is only needed to enable "zephyr" target
+# as OT platform for CMake
diff --git a/examples/platforms/zephyr/README.md b/examples/platforms/zephyr/README.md
new file mode 100644
index 0000000..b6e9828
--- /dev/null
+++ b/examples/platforms/zephyr/README.md
@@ -0,0 +1,3 @@
+The OpenThread stack is integrated with ZephyrOS and nRF Connect SDK.
+
+See the [Zephyr's OpenThread platform](https://github.com/zephyrproject-rtos/zephyr/tree/main/modules/openthread) and [CLI example](https://github.com/nrfconnect/sdk-nrf/tree/main/samples/openthread/cli) for more information about the integration.
diff --git a/include/Makefile.am b/include/Makefile.am
index 722775d..6ce6a7d 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -68,6 +68,7 @@
     openthread/link_metrics.h             \
     openthread/link_raw.h                 \
     openthread/logging.h                  \
+    openthread/mesh_diag.h                \
     openthread/message.h                  \
     openthread/multi_radio.h              \
     openthread/nat64.h                    \
@@ -102,6 +103,7 @@
     openthread/platform/crypto.h          \
     openthread/platform/debug_uart.h      \
     openthread/platform/diag.h            \
+    openthread/platform/dns.h             \
     openthread/platform/dso_transport.h   \
     openthread/platform/entropy.h         \
     openthread/platform/flash.h           \
diff --git a/include/openthread/BUILD.gn b/include/openthread/BUILD.gn
index 931c7ac..fba1dfe 100644
--- a/include/openthread/BUILD.gn
+++ b/include/openthread/BUILD.gn
@@ -72,6 +72,7 @@
     "link_metrics.h",
     "link_raw.h",
     "logging.h",
+    "mesh_diag.h",
     "message.h",
     "multi_radio.h",
     "nat64.h",
@@ -86,6 +87,7 @@
     "platform/crypto.h",
     "platform/debug_uart.h",
     "platform/diag.h",
+    "platform/dns.h",
     "platform/dso_transport.h",
     "platform/entropy.h",
     "platform/flash.h",
diff --git a/include/openthread/backbone_router_ftd.h b/include/openthread/backbone_router_ftd.h
index a619687..d4041d6 100644
--- a/include/openthread/backbone_router_ftd.h
+++ b/include/openthread/backbone_router_ftd.h
@@ -64,7 +64,14 @@
 } otBackboneRouterState;
 
 /**
- * This function enables or disables Backbone functionality.
+ * Enables or disables Backbone functionality.
+ *
+ * If enabled, a Server Data Request message `SRV_DATA.ntf` is triggered for the attached
+ * device if there is no Backbone Router Service in the Thread Network Data.
+ *
+ * If disabled, `SRV_DATA.ntf` is triggered if the Backbone Router is in the Primary state.
+ *
+ * Available when `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` is enabled.
  *
  * @param[in] aInstance A pointer to an OpenThread instance.
  * @param[in] aEnable   TRUE to enable Backbone functionality, FALSE otherwise.
@@ -78,7 +85,7 @@
 void otBackboneRouterSetEnabled(otInstance *aInstance, bool aEnable);
 
 /**
- * This function gets the Backbone Router state.
+ * Gets the Backbone Router #otBackboneRouterState.
  *
  * @param[in] aInstance       A pointer to an OpenThread instance.
  *
@@ -95,7 +102,9 @@
 otBackboneRouterState otBackboneRouterGetState(otInstance *aInstance);
 
 /**
- * This function gets the local Backbone Router configuration.
+ * Gets the local Backbone Router configuration.
+ *
+ * Available when `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` is enabled.
  *
  * @param[in]   aInstance            A pointer to an OpenThread instance.
  * @param[out]  aConfig              A pointer where to put local Backbone Router configuration.
@@ -110,7 +119,12 @@
 void otBackboneRouterGetConfig(otInstance *aInstance, otBackboneRouterConfig *aConfig);
 
 /**
- * This function sets the local Backbone Router configuration.
+ * Sets the local Backbone Router configuration #otBackboneRouterConfig.
+ *
+ * A Server Data Request message `SRV_DATA.ntf` is initiated automatically if BBR Dataset changes for Primary
+ * Backbone Router.
+ *
+ * Available when `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` is enabled.
  *
  * @param[in]  aInstance             A pointer to an OpenThread instance.
  * @param[in]  aConfig               A pointer to the Backbone Router configuration to take effect.
@@ -127,7 +141,11 @@
 otError otBackboneRouterSetConfig(otInstance *aInstance, const otBackboneRouterConfig *aConfig);
 
 /**
- * This function explicitly registers local Backbone Router configuration.
+ * Explicitly registers local Backbone Router configuration.
+ *
+ * A Server Data Request message `SRV_DATA.ntf` is triggered for the attached device.
+ *
+ * Available when `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` is enabled.
  *
  * @param[in]  aInstance             A pointer to an OpenThread instance.
  *
@@ -189,15 +207,16 @@
  *
  *
  */
-void otBackboneRouterConfigNextDuaRegistrationResponse(otInstance *                    aInstance,
+void otBackboneRouterConfigNextDuaRegistrationResponse(otInstance                     *aInstance,
                                                        const otIp6InterfaceIdentifier *aMlIid,
                                                        uint8_t                         aStatus);
 
 /**
- * This method configures response status for next Multicast Listener Registration.
+ * Configures the response status for the next Multicast Listener Registration.
  *
- * Note: available only when `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is enabled.
- *       Only used for test and certification.
+ * Available when `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE`,
+ * `OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE`, and
+ * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` are enabled.
  *
  * @param[in] aInstance  A pointer to an OpenThread instance.
  * @param[in] aStatus    The status to respond.
@@ -223,9 +242,9 @@
  * @param[in] aAddress  The IPv6 multicast address of the Multicast Listener.
  *
  */
-typedef void (*otBackboneRouterMulticastListenerCallback)(void *                                 aContext,
+typedef void (*otBackboneRouterMulticastListenerCallback)(void                                  *aContext,
                                                           otBackboneRouterMulticastListenerEvent aEvent,
-                                                          const otIp6Address *                   aAddress);
+                                                          const otIp6Address                    *aAddress);
 
 /**
  * This method sets the Backbone Router Multicast Listener callback.
@@ -235,15 +254,16 @@
  * @param[in] aContext   A user context pointer.
  *
  */
-void otBackboneRouterSetMulticastListenerCallback(otInstance *                              aInstance,
+void otBackboneRouterSetMulticastListenerCallback(otInstance                               *aInstance,
                                                   otBackboneRouterMulticastListenerCallback aCallback,
-                                                  void *                                    aContext);
+                                                  void                                     *aContext);
 
 /**
- * This method clears the Multicast Listeners.
+ * Clears the Multicast Listeners.
  *
- * Note: available only when `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is enabled.
- *       Only used for test and certification.
+ * Available when `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE`,
+ * `OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE`, and
+ * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` are enabled.
  *
  * @param[in] aInstance A pointer to an OpenThread instance.
  *
@@ -254,10 +274,13 @@
 void otBackboneRouterMulticastListenerClear(otInstance *aInstance);
 
 /**
- * This method adds a Multicast Listener.
+ * Adds a Multicast Listener with a timeout value, in seconds.
  *
- * Note: available only when `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is enabled.
- *       Only used for test and certification.
+ * Pass `0` to use the default MLR timeout.
+ *
+ * Available when `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE`,
+ * `OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE`, and
+ * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` are enabled.
  *
  * @param[in] aInstance  A pointer to an OpenThread instance.
  * @param[in] aAddress   The Multicast Listener address.
@@ -306,9 +329,9 @@
  * @sa otBackboneRouterMulticastListenerAdd
  *
  */
-otError otBackboneRouterMulticastListenerGetNext(otInstance *                               aInstance,
+otError otBackboneRouterMulticastListenerGetNext(otInstance                                *aInstance,
                                                  otBackboneRouterMulticastListenerIterator *aIterator,
-                                                 otBackboneRouterMulticastListenerInfo *    aListenerInfo);
+                                                 otBackboneRouterMulticastListenerInfo     *aListenerInfo);
 
 /**
  * Represents the ND Proxy events.
@@ -331,9 +354,9 @@
  *                      `OT_BACKBONE_ROUTER_NDPROXY_CLEARED`.
  *
  */
-typedef void (*otBackboneRouterNdProxyCallback)(void *                       aContext,
+typedef void (*otBackboneRouterNdProxyCallback)(void                        *aContext,
                                                 otBackboneRouterNdProxyEvent aEvent,
-                                                const otIp6Address *         aDua);
+                                                const otIp6Address          *aDua);
 
 /**
  * This method sets the Backbone Router ND Proxy callback.
@@ -343,9 +366,9 @@
  * @param[in] aContext   A user context pointer.
  *
  */
-void otBackboneRouterSetNdProxyCallback(otInstance *                    aInstance,
+void otBackboneRouterSetNdProxyCallback(otInstance                     *aInstance,
                                         otBackboneRouterNdProxyCallback aCallback,
-                                        void *                          aContext);
+                                        void                           *aContext);
 
 /**
  * Represents the Backbone Router ND Proxy info.
@@ -369,8 +392,8 @@
  * @retval OT_ERROR_NOT_FOUND  Failed to find the Domain Unicast Address in the ND Proxy table.
  *
  */
-otError otBackboneRouterGetNdProxyInfo(otInstance *                 aInstance,
-                                       const otIp6Address *         aDua,
+otError otBackboneRouterGetNdProxyInfo(otInstance                  *aInstance,
+                                       const otIp6Address          *aDua,
                                        otBackboneRouterNdProxyInfo *aNdProxyInfo);
 
 /**
@@ -392,9 +415,9 @@
  * @param[in] aDomainPrefix  The new Domain Prefix if added or changed, nullptr otherwise.
  *
  */
-typedef void (*otBackboneRouterDomainPrefixCallback)(void *                            aContext,
+typedef void (*otBackboneRouterDomainPrefixCallback)(void                             *aContext,
                                                      otBackboneRouterDomainPrefixEvent aEvent,
-                                                     const otIp6Prefix *               aDomainPrefix);
+                                                     const otIp6Prefix                *aDomainPrefix);
 /**
  * This method sets the Backbone Router Domain Prefix callback.
  *
@@ -403,9 +426,9 @@
  * @param[in] aContext   A user context pointer.
  *
  */
-void otBackboneRouterSetDomainPrefixCallback(otInstance *                         aInstance,
+void otBackboneRouterSetDomainPrefixCallback(otInstance                          *aInstance,
                                              otBackboneRouterDomainPrefixCallback aCallback,
-                                             void *                               aContext);
+                                             void                                *aContext);
 
 /**
  * @}
diff --git a/include/openthread/border_agent.h b/include/openthread/border_agent.h
index 83babfa..e412f6d 100644
--- a/include/openthread/border_agent.h
+++ b/include/openthread/border_agent.h
@@ -52,6 +52,12 @@
  */
 
 /**
+ * The length of Border Agent/Router ID in bytes.
+ *
+ */
+#define OT_BORDER_AGENT_ID_LENGTH (16)
+
+/**
  * This enumeration defines the Border Agent state.
  *
  */
@@ -83,6 +89,24 @@
 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.
+ * @param[inout] aLength    Specifies the length of `aId` when used as input and receives the length
+ *                          actual ID data copied to `aId` when used as output.
+ *
+ * @retval OT_ERROR_INVALID_ARGS  If value of `aLength` if smaller than `OT_BORDER_AGENT_ID_LENGTH`.
+ * @retval OT_ERROR_NONE          If successfully retrieved the Border Agent ID.
+ *
+ */
+otError otBorderAgentGetId(otInstance *aInstance, uint8_t *aId, uint16_t *aLength);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/border_router.h b/include/openthread/border_router.h
index fb7dab0..a945f91 100644
--- a/include/openthread/border_router.h
+++ b/include/openthread/border_router.h
@@ -105,9 +105,9 @@
  * @retval OT_ERROR_NOT_FOUND  No subsequent On Mesh prefix exists in the Thread Network Data.
  *
  */
-otError otBorderRouterGetNextOnMeshPrefix(otInstance *           aInstance,
+otError otBorderRouterGetNextOnMeshPrefix(otInstance            *aInstance,
                                           otNetworkDataIterator *aIterator,
-                                          otBorderRouterConfig * aConfig);
+                                          otBorderRouterConfig  *aConfig);
 
 /**
  * Add an external route configuration to the local network data.
@@ -150,7 +150,7 @@
  * @retval OT_ERROR_NOT_FOUND  No subsequent external route entry exists in the Thread Network Data.
  *
  */
-otError otBorderRouterGetNextRoute(otInstance *           aInstance,
+otError otBorderRouterGetNextRoute(otInstance            *aInstance,
                                    otNetworkDataIterator *aIterator,
                                    otExternalRouteConfig *aConfig);
 
diff --git a/include/openthread/border_routing.h b/include/openthread/border_routing.h
index 8cfec1b..961554e 100644
--- a/include/openthread/border_routing.h
+++ b/include/openthread/border_routing.h
@@ -107,6 +107,18 @@
 } otBorderRoutingPrefixTableEntry;
 
 /**
+ * This enumeration represents the state of Border Routing Manager.
+ *
+ */
+typedef enum
+{
+    OT_BORDER_ROUTING_STATE_UNINITIALIZED, ///< Routing Manager is uninitialized.
+    OT_BORDER_ROUTING_STATE_DISABLED,      ///< Routing Manager is initialized but disabled.
+    OT_BORDER_ROUTING_STATE_STOPPED,       ///< Routing Manager in initialized and enabled but currently stopped.
+    OT_BORDER_ROUTING_STATE_RUNNING,       ///< Routing Manager is initialized, enabled, and running.
+} otBorderRoutingState;
+
+/**
  * This method initializes the Border Routing Manager on given infrastructure interface.
  *
  * @note  This method MUST be called before any other otBorderRouting* APIs.
@@ -141,23 +153,37 @@
 otError otBorderRoutingSetEnabled(otInstance *aInstance, bool aEnabled);
 
 /**
- * This function gets the preference used when advertising Route Info Options (e.g., for discovered OMR prefixes) in
- * Router Advertisement messages sent over the infrastructure link.
+ * Gets the current state of Border Routing Manager.
  *
- * @param[in] aInstance A pointer to an OpenThread instance.
+ * @param[in]  aInstance  A pointer to an OpenThread instance.
  *
- * @returns The OMR prefix advertisement preference.
+ * @returns The current state of Border Routing Manager.
+ *
+ */
+otBorderRoutingState otBorderRoutingGetState(otInstance *aInstance);
+
+/**
+ * This function gets the current preference used when advertising Route Info Options (RIO) in Router Advertisement
+ * messages sent over the infrastructure link.
+ *
+ * The RIO preference is determined as follows:
+ *
+ * - If explicitly set by user by calling `otBorderRoutingSetRouteInfoOptionPreference()`, the given preference is
+ *   used.
+ * - Otherwise, it is determined based on device's current role: Medium preference when in router/leader role and
+ *   low preference when in child role.
+ *
+ * @returns The current Route Info Option preference.
  *
  */
 otRoutePreference otBorderRoutingGetRouteInfoOptionPreference(otInstance *aInstance);
 
 /**
- * This function sets the preference to use when advertising Route Info Options in Router Advertisement messages sent
- * over the infrastructure link, for example for discovered OMR prefixes.
+ * This function explicitly sets the preference to use when advertising Route Info Options (RIO) in Router
+ * Advertisement messages sent over the infrastructure link.
  *
- * By default BR will use `medium` preference level, but this function allows the default value to be changed. As an
- * example, it can be set to `low` preference in the case where device is a temporary BR (a mobile BR or a
- * battery-powered BR) to indicate that other BRs (if any) should be preferred over this BR on the infrastructure link.
+ * After a call to this function, BR will use the given preference for all its advertised RIOs. The preference can be
+ * cleared by calling `otBorderRoutingClearRouteInfoOptionPreference()`.
  *
  * @param[in] aInstance     A pointer to an OpenThread instance.
  * @param[in] aPreference   The route preference to use.
@@ -166,6 +192,17 @@
 void otBorderRoutingSetRouteInfoOptionPreference(otInstance *aInstance, otRoutePreference aPreference);
 
 /**
+ * This function clears a previously set preference value for advertised Route Info Options.
+ *
+ * After a call to this function, BR will use device's role to determine the RIO preference: Medium preference when
+ * in router/leader role and low preference when in child role.
+ *
+ * @param[in] aInstance     A pointer to an OpenThread instance.
+ *
+ */
+void otBorderRoutingClearRouteInfoOptionPreference(otInstance *aInstance);
+
+/**
  * Gets the local Off-Mesh-Routable (OMR) Prefix, for example `fdfc:1ff5:1512:5622::/64`.
  *
  * An OMR Prefix is a randomly generated 64-bit prefix that's published in the
@@ -190,33 +227,47 @@
  * @param[out]  aPrefix      A pointer to output the favored OMR prefix.
  * @param[out]  aPreference  A pointer to output the preference associated the favored prefix.
  *
- * @retval  OT_ERROR_INVALID_STATE  The Border Routing Manager is not initialized yet.
+ * @retval  OT_ERROR_INVALID_STATE  The Border Routing Manager is not running yet.
  * @retval  OT_ERROR_NONE           Successfully retrieved the favored OMR prefix.
  *
  */
 otError otBorderRoutingGetFavoredOmrPrefix(otInstance *aInstance, otIp6Prefix *aPrefix, otRoutePreference *aPreference);
 
 /**
- * Gets the On-Link Prefix for the adjacent infrastructure link, for example `fd41:2650:a6f5:0::/64`.
+ * Gets the local On-Link Prefix for the adjacent infrastructure link.
  *
- * An On-Link Prefix is a 64-bit prefix that's advertised on the infrastructure link if there isn't already a usable
- * on-link prefix being advertised on the link.
+ * The local On-Link Prefix is a 64-bit prefix that's advertised on the infrastructure link if there isn't already a
+ * usable on-link prefix being advertised on the link.
  *
  * @param[in]   aInstance  A pointer to an OpenThread instance.
  * @param[out]  aPrefix    A pointer to where the prefix will be output to.
  *
  * @retval  OT_ERROR_INVALID_STATE  The Border Routing Manager is not initialized yet.
- * @retval  OT_ERROR_NONE           Successfully retrieved the on-link prefix.
+ * @retval  OT_ERROR_NONE           Successfully retrieved the local on-link prefix.
  *
  */
 otError otBorderRoutingGetOnLinkPrefix(otInstance *aInstance, otIp6Prefix *aPrefix);
 
 /**
+ * Gets the currently favored On-Link Prefix.
+ *
+ * The favored prefix is either a discovered on-link prefix on the infrastructure link or the local on-link prefix.
+ *
+ * @param[in]   aInstance  A pointer to an OpenThread instance.
+ * @param[out]  aPrefix    A pointer to where the prefix will be output to.
+ *
+ * @retval  OT_ERROR_INVALID_STATE  The Border Routing Manager is not initialized yet.
+ * @retval  OT_ERROR_NONE           Successfully retrieved the favored on-link prefix.
+ *
+ */
+otError otBorderRoutingGetFavoredOnLinkPrefix(otInstance *aInstance, otIp6Prefix *aPrefix);
+
+/**
  * Gets the local NAT64 Prefix of the Border Router.
  *
  * NAT64 Prefix might not be advertised in the Thread network.
  *
- * `OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE` must be enabled.
+ * `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` must be enabled.
  *
  * @param[in]   aInstance   A pointer to an OpenThread instance.
  * @param[out]  aPrefix     A pointer to where the prefix will be output to.
@@ -228,6 +279,23 @@
 otError otBorderRoutingGetNat64Prefix(otInstance *aInstance, otIp6Prefix *aPrefix);
 
 /**
+ * Gets the currently favored NAT64 prefix.
+ *
+ * The favored NAT64 prefix can be discovered from infrastructure link or can be this device's local NAT64 prefix.
+ *
+ * @param[in]   aInstance    A pointer to an OpenThread instance.
+ * @param[out]  aPrefix      A pointer to output the favored NAT64 prefix.
+ * @param[out]  aPreference  A pointer to output the preference associated the favored prefix.
+ *
+ * @retval  OT_ERROR_INVALID_STATE  The Border Routing Manager is not initialized yet.
+ * @retval  OT_ERROR_NONE           Successfully retrieved the favored NAT64 prefix.
+ *
+ */
+otError otBorderRoutingGetFavoredNat64Prefix(otInstance        *aInstance,
+                                             otIp6Prefix       *aPrefix,
+                                             otRoutePreference *aPreference);
+
+/**
  * This function initializes an `otBorderRoutingPrefixTableIterator`.
  *
  * An iterator MUST be initialized before it is used.
@@ -254,9 +322,9 @@
  * @retval OT_ERROR_NOT_FOUND   No more entries in the table.
  *
  */
-otError otBorderRoutingGetNextPrefixTableEntry(otInstance *                        aInstance,
+otError otBorderRoutingGetNextPrefixTableEntry(otInstance                         *aInstance,
                                                otBorderRoutingPrefixTableIterator *aIterator,
-                                               otBorderRoutingPrefixTableEntry *   aEntry);
+                                               otBorderRoutingPrefixTableEntry    *aEntry);
 
 /**
  * @}
diff --git a/include/openthread/channel_manager.h b/include/openthread/channel_manager.h
index f7fb8e1..7307458 100644
--- a/include/openthread/channel_manager.h
+++ b/include/openthread/channel_manager.h
@@ -55,12 +55,12 @@
  */
 
 /**
- * This function requests a Thread network channel change.
+ * Requests a Thread network channel change.
  *
- * The network switches to the given channel after a specified delay (see otChannelManagerSetDelay()). The channel
+ * The network switches to the given channel after a specified delay (see #otChannelManagerSetDelay()). The channel
  * change is performed by updating the Pending Operational Dataset.
  *
- * A subsequent call to this function will cancel an ongoing previously requested channel change.
+ * A subsequent call will cancel an ongoing previously requested channel change.
  *
  * @param[in]  aInstance          A pointer to an OpenThread instance.
  * @param[in]  aChannel           The new channel for the Thread network.
@@ -87,9 +87,9 @@
 uint16_t otChannelManagerGetDelay(otInstance *aInstance);
 
 /**
- * This function sets the delay (in seconds) used for a channel change.
+ * Sets the delay (in seconds) used for a channel change.
  *
- * The delay should preferably be longer than maximum data poll interval used by all sleepy-end-devices within the
+ * The delay should preferably be longer than the maximum data poll interval used by all sleepy-end-devices within the
  * Thread network.
  *
  * @param[in]  aInstance          A pointer to an OpenThread instance.
@@ -105,7 +105,7 @@
  * This function requests that `ChannelManager` checks and selects a new channel and starts a channel change.
  *
  * Unlike the `otChannelManagerRequestChannelChange()` where the channel must be given as a parameter, this function
- * asks the `ChannelManager` to select a channel by itself (based of collected channel quality info).
+ * asks the `ChannelManager` to select a channel by itself (based on collected channel quality info).
  *
  * Once called, the Channel Manager will perform the following 3 steps:
  *
@@ -132,7 +132,7 @@
 otError otChannelManagerRequestChannelSelect(otInstance *aInstance, bool aSkipQualityCheck);
 
 /**
- * This function enables/disables the auto-channel-selection functionality.
+ * Enables or disables the auto-channel-selection functionality.
  *
  * When enabled, `ChannelManager` will periodically invoke a `RequestChannelSelect(false)`. The period interval
  * can be set by `SetAutoChannelSelectionInterval()`.
@@ -154,7 +154,7 @@
 bool otChannelManagerGetAutoChannelSelectionEnabled(otInstance *aInstance);
 
 /**
- * This function sets the period interval (in seconds) used by auto-channel-selection functionality.
+ * Sets the period interval (in seconds) used by auto-channel-selection functionality.
  *
  * @param[in] aInstance   A pointer to an OpenThread instance.
  * @param[in] aInterval   The interval in seconds.
@@ -176,7 +176,7 @@
 uint32_t otChannelManagerGetAutoChannelSelectionInterval(otInstance *aInstance);
 
 /**
- * This function gets the supported channel mask.
+ * Gets the supported channel mask.
  *
  * @param[in]  aInstance       A pointer to an OpenThread instance.
  *
@@ -186,7 +186,7 @@
 uint32_t otChannelManagerGetSupportedChannels(otInstance *aInstance);
 
 /**
- * This function sets the supported channel mask.
+ * Sets the supported channel mask.
  *
  * @param[in]  aInstance     A pointer to an OpenThread instance.
  * @param[in]  aChannelMask  A channel mask.
@@ -195,7 +195,7 @@
 void otChannelManagerSetSupportedChannels(otInstance *aInstance, uint32_t aChannelMask);
 
 /**
- * This function gets the favored channel mask.
+ * Gets the favored channel mask.
  *
  * @param[in]  aInstance       A pointer to an OpenThread instance.
  *
@@ -205,7 +205,7 @@
 uint32_t otChannelManagerGetFavoredChannels(otInstance *aInstance);
 
 /**
- * This function sets the favored channel mask.
+ * Sets the favored channel mask.
  *
  * @param[in]  aInstance     A pointer to an OpenThread instance.
  * @param[in]  aChannelMask  A channel mask.
@@ -214,7 +214,7 @@
 void otChannelManagerSetFavoredChannels(otInstance *aInstance, uint32_t aChannelMask);
 
 /**
- * This function gets the CCA failure rate threshold
+ * Gets the CCA failure rate threshold.
  *
  * @param[in]  aInstance     A pointer to an OpenThread instance.
  *
@@ -224,7 +224,7 @@
 uint16_t otChannelManagerGetCcaFailureRateThreshold(otInstance *aInstance);
 
 /**
- * This function sets the CCA failure rate threshold
+ * Sets the CCA failure rate threshold.
  *
  * @param[in]  aInstance     A pointer to an OpenThread instance.
  * @param[in]  aThreshold    A CCA failure rate threshold. Value 0 maps to 0% and 0xffff maps to 100%.
diff --git a/include/openthread/channel_monitor.h b/include/openthread/channel_monitor.h
index 0c8929c..12fa92a 100644
--- a/include/openthread/channel_monitor.h
+++ b/include/openthread/channel_monitor.h
@@ -64,13 +64,13 @@
  */
 
 /**
- * This function enables/disables the Channel Monitoring operation.
+ * Enables or disables the Channel Monitoring operation.
  *
  * Once operation starts, any previously collected data is cleared. However, after operation is disabled, the previous
  * collected data is still valid and can be read.
  *
- * @note OpenThread core internally enables/disables the Channel Monitoring operation when the IPv6 interface is
- * brought up/down (i.e., call to `otIp6SetEnabled()`).
+ * @note OpenThread core internally enables or disables the Channel Monitoring operation when the IPv6 interface is
+ * brought up or down, for example in a call to `otIp6SetEnabled()`.
  *
  * @param[in]  aInstance       A pointer to an OpenThread instance.
  * @param[in]  aEnabled        TRUE to enable/start Channel Monitoring operation, FALSE to disable/stop it.
diff --git a/include/openthread/child_supervision.h b/include/openthread/child_supervision.h
index 5c01721..100e028 100644
--- a/include/openthread/child_supervision.h
+++ b/include/openthread/child_supervision.h
@@ -29,7 +29,7 @@
 /**
  * @file
  * @brief
- *   This file includes the OpenThread API for child supervision feature
+ *   This file includes the OpenThread API for child supervision feature.
  */
 
 #ifndef OPENTHREAD_CHILD_SUPERVISION_H_
@@ -47,38 +47,35 @@
  * @brief
  *   This module includes functions for child supervision feature.
  *
- *   The functions in this module are available when child supervision feature
- *   (`OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE`) is enabled.
- *
  * @{
  *
  */
 
 /**
- * Get the child supervision interval (in seconds).
+ * Gets the child supervision interval (in seconds) on a child.
  *
- * Child supervision feature provides a mechanism for parent to ensure that a message is sent to each sleepy child
- * within the supervision interval. If there is no transmission to the child within the supervision interval,
- * OpenThread enqueues and sends a supervision message (a data message with empty payload) to the child.
+ * Child supervision feature provides a mechanism for a sleepy child to ask its parent to ensure to send a message to
+ * it within the supervision interval. If there is no transmission to the child within the supervision interval,
+ * parent sends a supervision message (a data message with empty payload) to the child.
  *
  * @param[in]  aInstance       A pointer to an OpenThread instance.
  *
- * @returns  The child supervision interval. Zero indicates that child supervision is disabled.
+ * @returns  The child supervision interval. Zero indicates that supervision is disabled.
  *
  */
 uint16_t otChildSupervisionGetInterval(otInstance *aInstance);
 
 /**
- * Set the child supervision interval (in seconds).
+ * Sets the child supervision interval (in seconds) on the child.
  *
  * @param[in]  aInstance       A pointer to an OpenThread instance.
- * @param[in]  aInterval       The supervision interval (in seconds). Zero to disable supervision on parent.
+ * @param[in]  aInterval       The supervision interval (in seconds). Zero to disable supervision.
  *
  */
 void otChildSupervisionSetInterval(otInstance *aInstance, uint16_t aInterval);
 
 /**
- * Get the supervision check timeout interval (in seconds).
+ * Gets the supervision check timeout interval (in seconds) on the child.
  *
  * If the device is a sleepy child and it does not hear from its parent within the specified check timeout, it initiates
  * the re-attach process (MLE Child Update Request/Response exchange with its parent).
@@ -91,7 +88,7 @@
 uint16_t otChildSupervisionGetCheckTimeout(otInstance *aInstance);
 
 /**
- * Set the supervision check timeout interval (in seconds).
+ * Sets the supervision check timeout interval (in seconds).
  *
  * @param[in]  aInstance       A pointer to an OpenThread instance.
  * @param[in]  aTimeout        The check timeout (in seconds). Zero to disable supervision check on the child.
@@ -100,6 +97,21 @@
 void otChildSupervisionSetCheckTimeout(otInstance *aInstance, uint16_t aTimeout);
 
 /**
+ * Get the value of supervision check timeout failure counter.
+ *
+ * The counter tracks the number of supervision check failures on the child. It is incremented when the child does
+ * not hear from its parent within the specified check timeout interval.
+ *
+ */
+uint16_t otChildSupervisionGetCheckFailureCounter(otInstance *aInstance);
+
+/**
+ * Reset the supervision check timeout failure counter to zero.
+ *
+ */
+void otChildSupervisionResetCheckFailureCounter(otInstance *aInstance);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/cli.h b/include/openthread/cli.h
index 3fa5c56..e0d9e66 100644
--- a/include/openthread/cli.h
+++ b/include/openthread/cli.h
@@ -52,9 +52,9 @@
 typedef struct otCliCommand
 {
     const char *mName; ///< A pointer to the command string.
-    void (*mCommand)(void *  aContext,
-                     uint8_t aArgsLength,
-                     char *  aArgs[]); ///< A function pointer to process the command.
+    otError (*mCommand)(void   *aContext,
+                        uint8_t aArgsLength,
+                        char   *aArgs[]); ///< A function pointer to process the command.
 } otCliCommand;
 
 /**
@@ -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.
diff --git a/include/openthread/coap.h b/include/openthread/coap.h
index c2e036b..30faca6 100644
--- a/include/openthread/coap.h
+++ b/include/openthread/coap.h
@@ -341,8 +341,8 @@
  * @retval  OT_ERROR_RESPONSE_TIMEOUT  No response or acknowledgment received during timeout period.
  *
  */
-typedef void (*otCoapResponseHandler)(void *               aContext,
-                                      otMessage *          aMessage,
+typedef void (*otCoapResponseHandler)(void                *aContext,
+                                      otMessage           *aMessage,
                                       const otMessageInfo *aMessageInfo,
                                       otError              aResult);
 
@@ -375,7 +375,7 @@
  * @retval  OT_ERROR_NO_FRAME_RECEIVED  Block segment missing.
  *
  */
-typedef otError (*otCoapBlockwiseReceiveHook)(void *         aContext,
+typedef otError (*otCoapBlockwiseReceiveHook)(void          *aContext,
                                               const uint8_t *aBlock,
                                               uint32_t       aPosition,
                                               uint16_t       aBlockLength,
@@ -402,11 +402,11 @@
  * @retval  OT_ERROR_INVALID_ARGS   Block at @p aPosition does not exist.
  *
  */
-typedef otError (*otCoapBlockwiseTransmitHook)(void *    aContext,
-                                               uint8_t * aBlock,
+typedef otError (*otCoapBlockwiseTransmitHook)(void     *aContext,
+                                               uint8_t  *aBlock,
                                                uint32_t  aPosition,
                                                uint16_t *aBlockLength,
-                                               bool *    aMore);
+                                               bool     *aMore);
 
 /**
  * This structure represents a CoAP resource.
@@ -414,9 +414,9 @@
  */
 typedef struct otCoapResource
 {
-    const char *           mUriPath; ///< The URI Path string
+    const char            *mUriPath; ///< The URI Path string
     otCoapRequestHandler   mHandler; ///< The callback for handling a received request
-    void *                 mContext; ///< Application-specific context
+    void                  *mContext; ///< Application-specific context
     struct otCoapResource *mNext;    ///< The next CoAP resource in the list
 } otCoapResource;
 
@@ -426,7 +426,7 @@
  */
 typedef struct otCoapBlockwiseResource
 {
-    const char *         mUriPath; ///< The URI Path string
+    const char          *mUriPath; ///< The URI Path string
     otCoapRequestHandler mHandler; ///< The callback for handling a received request
 
     /** The callback for handling incoming block-wise transfer.
@@ -440,7 +440,7 @@
      *  configuration is enabled.
      */
     otCoapBlockwiseTransmitHook     mTransmitHook;
-    void *                          mContext; ///< Application-specific context
+    void                           *mContext; ///< Application-specific context
     struct otCoapBlockwiseResource *mNext;    ///< The next CoAP resource in the list
 } otCoapBlockwiseResource;
 
@@ -883,11 +883,11 @@
  * @retval OT_ERROR_NO_BUFS         Failed to allocate retransmission data.
  *
  */
-otError otCoapSendRequestWithParameters(otInstance *              aInstance,
-                                        otMessage *               aMessage,
-                                        const otMessageInfo *     aMessageInfo,
+otError otCoapSendRequestWithParameters(otInstance               *aInstance,
+                                        otMessage                *aMessage,
+                                        const otMessageInfo      *aMessageInfo,
                                         otCoapResponseHandler     aHandler,
-                                        void *                    aContext,
+                                        void                     *aContext,
                                         const otCoapTxParameters *aTxParameters);
 
 /**
@@ -913,12 +913,12 @@
  * @retval OT_ERROR_NO_BUFS Failed to allocate retransmission data.
  *
  */
-otError otCoapSendRequestBlockWiseWithParameters(otInstance *                aInstance,
-                                                 otMessage *                 aMessage,
-                                                 const otMessageInfo *       aMessageInfo,
+otError otCoapSendRequestBlockWiseWithParameters(otInstance                 *aInstance,
+                                                 otMessage                  *aMessage,
+                                                 const otMessageInfo        *aMessageInfo,
                                                  otCoapResponseHandler       aHandler,
-                                                 void *                      aContext,
-                                                 const otCoapTxParameters *  aTxParameters,
+                                                 void                       *aContext,
+                                                 const otCoapTxParameters   *aTxParameters,
                                                  otCoapBlockwiseTransmitHook aTransmitHook,
                                                  otCoapBlockwiseReceiveHook  aReceiveHook);
 
@@ -944,11 +944,11 @@
  * @retval OT_ERROR_NO_BUFS Failed to allocate retransmission data.
  *
  */
-static inline otError otCoapSendRequestBlockWise(otInstance *                aInstance,
-                                                 otMessage *                 aMessage,
-                                                 const otMessageInfo *       aMessageInfo,
+static inline otError otCoapSendRequestBlockWise(otInstance                 *aInstance,
+                                                 otMessage                  *aMessage,
+                                                 const otMessageInfo        *aMessageInfo,
                                                  otCoapResponseHandler       aHandler,
-                                                 void *                      aContext,
+                                                 void                       *aContext,
                                                  otCoapBlockwiseTransmitHook aTransmitHook,
                                                  otCoapBlockwiseReceiveHook  aReceiveHook)
 {
@@ -973,11 +973,11 @@
  * @retval OT_ERROR_NO_BUFS Failed to allocate retransmission data.
  *
  */
-static inline otError otCoapSendRequest(otInstance *          aInstance,
-                                        otMessage *           aMessage,
-                                        const otMessageInfo * aMessageInfo,
+static inline otError otCoapSendRequest(otInstance           *aInstance,
+                                        otMessage            *aMessage,
+                                        const otMessageInfo  *aMessageInfo,
                                         otCoapResponseHandler aHandler,
-                                        void *                aContext)
+                                        void                 *aContext)
 {
     // NOLINTNEXTLINE(modernize-use-nullptr)
     return otCoapSendRequestWithParameters(aInstance, aMessage, aMessageInfo, aHandler, aContext, NULL);
@@ -1063,9 +1063,9 @@
  * @retval OT_ERROR_NO_BUFS  Insufficient buffers available to send the CoAP response.
  *
  */
-otError otCoapSendResponseWithParameters(otInstance *              aInstance,
-                                         otMessage *               aMessage,
-                                         const otMessageInfo *     aMessageInfo,
+otError otCoapSendResponseWithParameters(otInstance               *aInstance,
+                                         otMessage                *aMessage,
+                                         const otMessageInfo      *aMessageInfo,
                                          const otCoapTxParameters *aTxParameters);
 
 /**
@@ -1085,11 +1085,11 @@
  * @retval OT_ERROR_NO_BUFS  Insufficient buffers available to send the CoAP response.
  *
  */
-otError otCoapSendResponseBlockWiseWithParameters(otInstance *                aInstance,
-                                                  otMessage *                 aMessage,
-                                                  const otMessageInfo *       aMessageInfo,
-                                                  const otCoapTxParameters *  aTxParameters,
-                                                  void *                      aContext,
+otError otCoapSendResponseBlockWiseWithParameters(otInstance                 *aInstance,
+                                                  otMessage                  *aMessage,
+                                                  const otMessageInfo        *aMessageInfo,
+                                                  const otCoapTxParameters   *aTxParameters,
+                                                  void                       *aContext,
                                                   otCoapBlockwiseTransmitHook aTransmitHook);
 
 /**
@@ -1108,10 +1108,10 @@
  * @retval OT_ERROR_NO_BUFS  Insufficient buffers available to send the CoAP response.
  *
  */
-static inline otError otCoapSendResponseBlockWise(otInstance *                aInstance,
-                                                  otMessage *                 aMessage,
-                                                  const otMessageInfo *       aMessageInfo,
-                                                  void *                      aContext,
+static inline otError otCoapSendResponseBlockWise(otInstance                 *aInstance,
+                                                  otMessage                  *aMessage,
+                                                  const otMessageInfo        *aMessageInfo,
+                                                  void                       *aContext,
                                                   otCoapBlockwiseTransmitHook aTransmitHook)
 {
     // NOLINTNEXTLINE(modernize-use-nullptr)
diff --git a/include/openthread/coap_secure.h b/include/openthread/coap_secure.h
index 5cb6bd9..6f42409 100644
--- a/include/openthread/coap_secure.h
+++ b/include/openthread/coap_secure.h
@@ -108,7 +108,7 @@
  * @param[in]  aPskIdLength  The PSK Identity Length.
  *
  */
-void otCoapSecureSetPsk(otInstance *   aInstance,
+void otCoapSecureSetPsk(otInstance    *aInstance,
                         const uint8_t *aPsk,
                         uint16_t       aPskLength,
                         const uint8_t *aPskIdentity,
@@ -130,9 +130,9 @@
  * @retval OT_ERROR_NO_BUFS         Can't allocate memory for certificate.
  *
  */
-otError otCoapSecureGetPeerCertificateBase64(otInstance *   aInstance,
+otError otCoapSecureGetPeerCertificateBase64(otInstance    *aInstance,
                                              unsigned char *aPeerCert,
-                                             size_t *       aCertLength,
+                                             size_t        *aCertLength,
                                              size_t         aCertBufferSize);
 
 /**
@@ -160,7 +160,7 @@
  * @param[in]  aPrivateKeyLength  The length of the private key.
  *
  */
-void otCoapSecureSetCertificate(otInstance *   aInstance,
+void otCoapSecureSetCertificate(otInstance    *aInstance,
                                 const uint8_t *aX509Cert,
                                 uint32_t       aX509Length,
                                 const uint8_t *aPrivateKey,
@@ -179,7 +179,7 @@
  * @param[in]  aX509CaCertChainLength   The length of chain.
  *
  */
-void otCoapSecureSetCaCertificateChain(otInstance *   aInstance,
+void otCoapSecureSetCaCertificateChain(otInstance    *aInstance,
                                        const uint8_t *aX509CaCertificateChain,
                                        uint32_t       aX509CaCertChainLength);
 
@@ -195,10 +195,10 @@
  * @retval OT_ERROR_NONE  Successfully started DTLS connection.
  *
  */
-otError otCoapSecureConnect(otInstance *                    aInstance,
-                            const otSockAddr *              aSockAddr,
+otError otCoapSecureConnect(otInstance                     *aInstance,
+                            const otSockAddr               *aSockAddr,
                             otHandleCoapSecureClientConnect aHandler,
-                            void *                          aContext);
+                            void                           *aContext);
 
 /**
  * This method stops the DTLS connection.
@@ -252,10 +252,10 @@
  * @retval OT_ERROR_INVALID_STATE  DTLS connection was not initialized.
  *
  */
-otError otCoapSecureSendRequestBlockWise(otInstance *                aInstance,
-                                         otMessage *                 aMessage,
+otError otCoapSecureSendRequestBlockWise(otInstance                 *aInstance,
+                                         otMessage                  *aMessage,
                                          otCoapResponseHandler       aHandler,
-                                         void *                      aContext,
+                                         void                       *aContext,
                                          otCoapBlockwiseTransmitHook aTransmitHook,
                                          otCoapBlockwiseReceiveHook  aReceiveHook);
 
@@ -276,10 +276,10 @@
  * @retval OT_ERROR_INVALID_STATE  DTLS connection was not initialized.
  *
  */
-otError otCoapSecureSendRequest(otInstance *          aInstance,
-                                otMessage *           aMessage,
+otError otCoapSecureSendRequest(otInstance           *aInstance,
+                                otMessage            *aMessage,
                                 otCoapResponseHandler aHandler,
-                                void *                aContext);
+                                void                 *aContext);
 
 /**
  * This function adds a resource to the CoAP Secure server.
@@ -336,9 +336,9 @@
  * @param[in]  aContext      A pointer to arbitrary context information. May be NULL if not used.
  *
  */
-void otCoapSecureSetClientConnectedCallback(otInstance *                    aInstance,
+void otCoapSecureSetClientConnectedCallback(otInstance                     *aInstance,
                                             otHandleCoapSecureClientConnect aHandler,
-                                            void *                          aContext);
+                                            void                           *aContext);
 
 /**
  * This function sends a CoAP response block-wise from the CoAP Secure server.
@@ -356,10 +356,10 @@
  * @retval OT_ERROR_NO_BUFS  Insufficient buffers available to send the CoAP response.
  *
  */
-otError otCoapSecureSendResponseBlockWise(otInstance *                aInstance,
-                                          otMessage *                 aMessage,
-                                          const otMessageInfo *       aMessageInfo,
-                                          void *                      aContext,
+otError otCoapSecureSendResponseBlockWise(otInstance                 *aInstance,
+                                          otMessage                  *aMessage,
+                                          const otMessageInfo        *aMessageInfo,
+                                          void                       *aContext,
                                           otCoapBlockwiseTransmitHook aTransmitHook);
 
 /**
diff --git a/include/openthread/commissioner.h b/include/openthread/commissioner.h
index d632fd0..9bf09b8 100644
--- a/include/openthread/commissioner.h
+++ b/include/openthread/commissioner.h
@@ -170,9 +170,9 @@
  *
  */
 typedef void (*otCommissionerJoinerCallback)(otCommissionerJoinerEvent aEvent,
-                                             const otJoinerInfo *      aJoinerInfo,
-                                             const otExtAddress *      aJoinerId,
-                                             void *                    aContext);
+                                             const otJoinerInfo       *aJoinerInfo,
+                                             const otExtAddress       *aJoinerId,
+                                             void                     *aContext);
 
 /**
  * This function enables the Thread Commissioner role.
@@ -187,10 +187,10 @@
  * @retval OT_ERROR_INVALID_STATE  Device is not currently attached to a network.
  *
  */
-otError otCommissionerStart(otInstance *                 aInstance,
+otError otCommissionerStart(otInstance                  *aInstance,
                             otCommissionerStateCallback  aStateCallback,
                             otCommissionerJoinerCallback aJoinerCallback,
-                            void *                       aCallbackContext);
+                            void                        *aCallbackContext);
 
 /**
  * This function disables the Thread Commissioner role.
@@ -242,9 +242,9 @@
  * @note Only use this after successfully starting the Commissioner role with otCommissionerStart().
  *
  */
-otError otCommissionerAddJoiner(otInstance *        aInstance,
+otError otCommissionerAddJoiner(otInstance         *aInstance,
                                 const otExtAddress *aEui64,
-                                const char *        aPskd,
+                                const char         *aPskd,
                                 uint32_t            aTimeout);
 
 /**
@@ -263,9 +263,9 @@
  * @note Only use this after successfully starting the Commissioner role with otCommissionerStart().
  *
  */
-otError otCommissionerAddJoinerWithDiscerner(otInstance *             aInstance,
+otError otCommissionerAddJoinerWithDiscerner(otInstance              *aInstance,
                                              const otJoinerDiscerner *aDiscerner,
-                                             const char *             aPskd,
+                                             const char              *aPskd,
                                              uint32_t                 aTimeout);
 
 /**
@@ -351,7 +351,7 @@
  * @note Only use this after successfully starting the Commissioner role with otCommissionerStart().
  *
  */
-otError otCommissionerAnnounceBegin(otInstance *        aInstance,
+otError otCommissionerAnnounceBegin(otInstance         *aInstance,
                                     uint32_t            aChannelMask,
                                     uint8_t             aCount,
                                     uint16_t            aPeriod,
@@ -369,7 +369,7 @@
 typedef void (*otCommissionerEnergyReportCallback)(uint32_t       aChannelMask,
                                                    const uint8_t *aEnergyList,
                                                    uint8_t        aEnergyListLength,
-                                                   void *         aContext);
+                                                   void          *aContext);
 
 /**
  * This function sends an Energy Scan Query message.
@@ -390,14 +390,14 @@
  * @note Only use this after successfully starting the Commissioner role with otCommissionerStart().
  *
  */
-otError otCommissionerEnergyScan(otInstance *                       aInstance,
+otError otCommissionerEnergyScan(otInstance                        *aInstance,
                                  uint32_t                           aChannelMask,
                                  uint8_t                            aCount,
                                  uint16_t                           aPeriod,
                                  uint16_t                           aScanDuration,
-                                 const otIp6Address *               aAddress,
+                                 const otIp6Address                *aAddress,
                                  otCommissionerEnergyReportCallback aCallback,
-                                 void *                             aContext);
+                                 void                              *aContext);
 
 /**
  * This function pointer is called when the Commissioner receives a PAN ID Conflict message.
@@ -426,12 +426,12 @@
  * @note Only use this after successfully starting the Commissioner role with otCommissionerStart().
  *
  */
-otError otCommissionerPanIdQuery(otInstance *                        aInstance,
+otError otCommissionerPanIdQuery(otInstance                         *aInstance,
                                  uint16_t                            aPanId,
                                  uint32_t                            aChannelMask,
-                                 const otIp6Address *                aAddress,
+                                 const otIp6Address                 *aAddress,
                                  otCommissionerPanIdConflictCallback aCallback,
-                                 void *                              aContext);
+                                 void                               *aContext);
 
 /**
  * This function sends MGMT_COMMISSIONER_GET.
@@ -460,9 +460,9 @@
  * @retval OT_ERROR_INVALID_STATE The commissioner is not active.
  *
  */
-otError otCommissionerSendMgmtSet(otInstance *                  aInstance,
+otError otCommissionerSendMgmtSet(otInstance                   *aInstance,
                                   const otCommissioningDataset *aDataset,
-                                  const uint8_t *               aTlvs,
+                                  const uint8_t                *aTlvs,
                                   uint8_t                       aLength);
 
 /**
diff --git a/include/openthread/crypto.h b/include/openthread/crypto.h
index 3be657b..b262926 100644
--- a/include/openthread/crypto.h
+++ b/include/openthread/crypto.h
@@ -55,25 +55,13 @@
  *
  */
 
-#define OT_CRYPTO_SHA256_HASH_SIZE 32 ///< Length of SHA256 hash (in bytes).
-
 /**
  * @struct otCryptoSha256Hash
  *
  * This structure represents a SHA-256 hash.
  *
  */
-OT_TOOL_PACKED_BEGIN
-struct otCryptoSha256Hash
-{
-    uint8_t m8[OT_CRYPTO_SHA256_HASH_SIZE]; ///< Hash bytes.
-} OT_TOOL_PACKED_END;
-
-/**
- * This structure represents a SHA-256 hash.
- *
- */
-typedef struct otCryptoSha256Hash otCryptoSha256Hash;
+typedef otPlatCryptoSha256Hash otCryptoSha256Hash;
 
 /**
  * This function performs HMAC computation.
@@ -107,37 +95,15 @@
  */
 void otCryptoAesCcm(const otCryptoKey *aKey,
                     uint8_t            aTagLength,
-                    const void *       aNonce,
+                    const void        *aNonce,
                     uint8_t            aNonceLength,
-                    const void *       aHeader,
+                    const void        *aHeader,
                     uint32_t           aHeaderLength,
-                    void *             aPlainText,
-                    void *             aCipherText,
+                    void              *aPlainText,
+                    void              *aCipherText,
                     uint32_t           aLength,
                     bool               aEncrypt,
-                    void *             aTag);
-
-/**
- * This method creates ECDSA sign.
- *
- * @param[out]     aOutput            An output buffer where ECDSA sign should be stored.
- * @param[in,out]  aOutputLength      The length of the @p aOutput buffer.
- * @param[in]      aInputHash         An input hash.
- * @param[in]      aInputHashLength   The length of the @p aInputHash buffer.
- * @param[in]      aPrivateKey        A private key in PEM format.
- * @param[in]      aPrivateKeyLength  The length of the @p aPrivateKey buffer.
- *
- * @retval  OT_ERROR_NONE         ECDSA sign has been created successfully.
- * @retval  OT_ERROR_NO_BUFS      Output buffer is too small.
- * @retval  OT_ERROR_INVALID_ARGS Private key is not valid EC Private Key.
- * @retval  OT_ERROR_FAILED       Error during signing.
- */
-otError otCryptoEcdsaSign(uint8_t *      aOutput,
-                          uint16_t *     aOutputLength,
-                          const uint8_t *aInputHash,
-                          uint16_t       aInputHashLength,
-                          const uint8_t *aPrivateKey,
-                          uint16_t       aPrivateKeyLength);
+                    void              *aTag);
 
 /**
  * @}
diff --git a/include/openthread/dataset.h b/include/openthread/dataset.h
index 63e1e26..efd18bc 100644
--- a/include/openthread/dataset.h
+++ b/include/openthread/dataset.h
@@ -49,6 +49,8 @@
  *
  * @{
  *
+ * For FTD and MTD builds, the Operational Dataset API includes functions to manage Active and Pending datasets
+ * and dataset TLVs.
  */
 
 #define OT_NETWORK_KEY_SIZE 16 ///< Size of the Thread Network Key (bytes)
@@ -227,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
@@ -339,7 +341,7 @@
 bool otDatasetIsCommissioned(otInstance *aInstance);
 
 /**
- * This function gets the Active Operational Dataset.
+ * Gets the Active Operational Dataset.
  *
  * @param[in]   aInstance A pointer to an OpenThread instance.
  * @param[out]  aDataset  A pointer to where the Active Operational Dataset will be placed.
@@ -363,7 +365,7 @@
 otError otDatasetGetActiveTlvs(otInstance *aInstance, otOperationalDatasetTlvs *aDataset);
 
 /**
- * This function sets the Active Operational Dataset.
+ * Sets the Active Operational Dataset.
  *
  * If the dataset does not include an Active Timestamp, the dataset is only partially complete.
  *
@@ -439,7 +441,7 @@
 otError otDatasetGetPendingTlvs(otInstance *aInstance, otOperationalDatasetTlvs *aDataset);
 
 /**
- * This function sets the Pending Operational Dataset.
+ * Sets the Pending Operational Dataset.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  * @param[in]  aDataset  A pointer to the Pending Operational Dataset.
@@ -465,7 +467,7 @@
 otError otDatasetSetPendingTlvs(otInstance *aInstance, const otOperationalDatasetTlvs *aDataset);
 
 /**
- * This function sends MGMT_ACTIVE_GET.
+ * Sends MGMT_ACTIVE_GET.
  *
  * @param[in]  aInstance           A pointer to an OpenThread instance.
  * @param[in]  aDatasetComponents  A pointer to a Dataset Components structure specifying which components to request.
@@ -477,14 +479,14 @@
  * @retval OT_ERROR_NO_BUFS       Insufficient buffer space to send.
  *
  */
-otError otDatasetSendMgmtActiveGet(otInstance *                          aInstance,
+otError otDatasetSendMgmtActiveGet(otInstance                           *aInstance,
                                    const otOperationalDatasetComponents *aDatasetComponents,
-                                   const uint8_t *                       aTlvTypes,
+                                   const uint8_t                        *aTlvTypes,
                                    uint8_t                               aLength,
-                                   const otIp6Address *                  aAddress);
+                                   const otIp6Address                   *aAddress);
 
 /**
- * This function sends MGMT_ACTIVE_SET.
+ * Sends MGMT_ACTIVE_SET.
  *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  * @param[in]  aDataset   A pointer to operational dataset.
@@ -498,15 +500,15 @@
  * @retval OT_ERROR_BUSY          A previous request is ongoing.
  *
  */
-otError otDatasetSendMgmtActiveSet(otInstance *                aInstance,
+otError otDatasetSendMgmtActiveSet(otInstance                 *aInstance,
                                    const otOperationalDataset *aDataset,
-                                   const uint8_t *             aTlvs,
+                                   const uint8_t              *aTlvs,
                                    uint8_t                     aLength,
                                    otDatasetMgmtSetCallback    aCallback,
-                                   void *                      aContext);
+                                   void                       *aContext);
 
 /**
- * This function sends MGMT_PENDING_GET.
+ * Sends MGMT_PENDING_GET.
  *
  * @param[in]  aInstance           A pointer to an OpenThread instance.
  * @param[in]  aDatasetComponents  A pointer to a Dataset Components structure specifying which components to request.
@@ -518,14 +520,14 @@
  * @retval OT_ERROR_NO_BUFS       Insufficient buffer space to send.
  *
  */
-otError otDatasetSendMgmtPendingGet(otInstance *                          aInstance,
+otError otDatasetSendMgmtPendingGet(otInstance                           *aInstance,
                                     const otOperationalDatasetComponents *aDatasetComponents,
-                                    const uint8_t *                       aTlvTypes,
+                                    const uint8_t                        *aTlvTypes,
                                     uint8_t                               aLength,
-                                    const otIp6Address *                  aAddress);
+                                    const otIp6Address                   *aAddress);
 
 /**
- * This function sends MGMT_PENDING_SET.
+ * Sends MGMT_PENDING_SET.
  *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  * @param[in]  aDataset   A pointer to operational dataset.
@@ -539,12 +541,12 @@
  * @retval OT_ERROR_BUSY          A previous request is ongoing.
  *
  */
-otError otDatasetSendMgmtPendingSet(otInstance *                aInstance,
+otError otDatasetSendMgmtPendingSet(otInstance                 *aInstance,
                                     const otOperationalDataset *aDataset,
-                                    const uint8_t *             aTlvs,
+                                    const uint8_t              *aTlvs,
                                     uint8_t                     aLength,
                                     otDatasetMgmtSetCallback    aCallback,
-                                    void *                      aContext);
+                                    void                       *aContext);
 
 /**
  * This function generates PSKc from a given pass-phrase, network name, and extended PAN ID.
@@ -560,15 +562,15 @@
  * @retval OT_ERROR_INVALID_ARGS  If any of the input arguments is invalid.
  *
  */
-otError otDatasetGeneratePskc(const char *           aPassPhrase,
-                              const otNetworkName *  aNetworkName,
+otError otDatasetGeneratePskc(const char            *aPassPhrase,
+                              const otNetworkName   *aNetworkName,
                               const otExtendedPanId *aExtPanId,
-                              otPskc *               aPskc);
+                              otPskc                *aPskc);
 
 /**
- * This function sets an `otNetworkName` instance from a given null terminated C string.
+ * Sets an `otNetworkName` instance from a given null terminated C string.
  *
- * This function also validates that the given @p aNameString follows UTF-8 encoding and its length is not longer than
+ * @p aNameString must follow UTF-8 encoding and the Network Name length must not be longer than
  * `OT_NETWORK_NAME_MAX_SIZE`.
  *
  * @param[out] aNetworkName        A pointer to the `otNetworkName` to set.
@@ -581,7 +583,7 @@
 otError otNetworkNameFromString(otNetworkName *aNetworkName, const char *aNameString);
 
 /**
- * This function parses an Operational Dataset from a `otOperationalDatasetTlvs`.
+ * Parses an Operational Dataset from a given `otOperationalDatasetTlvs`.
  *
  * @param[in]  aDatasetTlvs  A pointer to dataset TLVs.
  * @param[out] aDataset      A pointer to where the dataset will be placed.
@@ -593,6 +595,33 @@
 otError otDatasetParseTlvs(const otOperationalDatasetTlvs *aDatasetTlvs, otOperationalDataset *aDataset);
 
 /**
+ * Converts a given Operational Dataset to `otOperationalDatasetTlvs`.
+ *
+ * @param[in]  aDataset      An Operational dataset to convert to TLVs.
+ * @param[out] aDatasetTlvs  A pointer to dataset TLVs to return the result.
+ *
+ * @retval OT_ERROR_NONE          Successfully converted @p aDataset and updated @p aDatasetTlvs.
+ * @retval OT_ERROR_INVALID_ARGS  @p aDataset is invalid, does not contain active or pending timestamps.
+ *
+ */
+otError otDatasetConvertToTlvs(const otOperationalDataset *aDataset, otOperationalDatasetTlvs *aDatasetTlvs);
+
+/**
+ * Updates a given Operational Dataset.
+ *
+ * @p aDataset contains the fields to be updated and their new value.
+ *
+ * @param[in]     aDataset      Specifies the set of types and values to update.
+ * @param[in,out] aDatasetTlvs  A pointer to dataset TLVs to update.
+ *
+ * @retval OT_ERROR_NONE          Successfully updated @p aDatasetTlvs.
+ * @retval OT_ERROR_INVALID_ARGS  @p aDataset contains invalid values.
+ * @retval OT_ERROR_NO_BUFS       Not enough space space in @p aDatasetTlvs to apply the update.
+ *
+ */
+otError otDatasetUpdateTlvs(const otOperationalDataset *aDataset, otOperationalDatasetTlvs *aDatasetTlvs);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/dataset_ftd.h b/include/openthread/dataset_ftd.h
index f1ae621..05e37a1 100644
--- a/include/openthread/dataset_ftd.h
+++ b/include/openthread/dataset_ftd.h
@@ -50,7 +50,7 @@
  */
 
 /**
- * This method creates a new Operational Dataset to use when forming a new network.
+ * For FTD only, creates a new Operational Dataset to use when forming a new network.
  *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  * @param[out] aDataset   The Operational Dataset.
@@ -62,7 +62,7 @@
 otError otDatasetCreateNewNetwork(otInstance *aInstance, otOperationalDataset *aDataset);
 
 /**
- * Get minimal delay timer.
+ * For FTD only, gets a minimal delay timer.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -72,7 +72,7 @@
 uint32_t otDatasetGetDelayTimerMinimal(otInstance *aInstance);
 
 /**
- * Set minimal delay timer.
+ * For FTD only, sets a minimal delay timer.
  *
  * @note This API is reserved for testing and demo purposes only. Changing settings with
  * this API will render a production application non-compliant with the Thread Specification.
diff --git a/include/openthread/dataset_updater.h b/include/openthread/dataset_updater.h
index 627ddb3..cb16c33 100644
--- a/include/openthread/dataset_updater.h
+++ b/include/openthread/dataset_updater.h
@@ -45,20 +45,18 @@
 /**
  * @addtogroup api-operational-dataset
  *
- * @brief
- *   This module includes functions for Dataset Updater.
- *
- *   The functions in this module are available when Dataset Updater feature is enabled (i.e.
- *   `OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE` is set to 1). Further this feature is available only on an FTD build.
- *
  * @{
  *
+ * For FTD builds only, Dataset Updater includes functions to manage dataset updates.
+ *
  */
 
 /**
  * This callback function pointer is called when a Dataset update request finishes, reporting success or failure status
  * of the Dataset update request.
  *
+ * Available when `OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE` is enabled.
+ *
  * @param[in] aError   The error status.
  *                     OT_ERROR_NONE            indicates successful Dataset update.
  *                     OT_ERROR_INVALID_STATE   indicates failure due invalid state (MLE being disabled).
@@ -73,6 +71,8 @@
 /**
  * This function requests an update to Operational Dataset.
  *
+ * Available when `OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE` is enabled.
+ *
  * @p aDataset should contain the fields to be updated and their new value. It must not contain Active or Pending
  * Timestamp fields. The Delay field is optional, if not provided a default value (1000 ms) would be used.
  *
@@ -88,14 +88,16 @@
  * @retval OT_ERROR_NO_BUFS        Could not allocated buffer to save Dataset.
  *
  */
-otError otDatasetUpdaterRequestUpdate(otInstance *                aInstance,
+otError otDatasetUpdaterRequestUpdate(otInstance                 *aInstance,
                                       const otOperationalDataset *aDataset,
                                       otDatasetUpdaterCallback    aCallback,
-                                      void *                      aContext);
+                                      void                       *aContext);
 
 /**
  * This function cancels an ongoing (if any) Operational Dataset update request.
  *
+ * Available when `OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE` is enabled.
+ *
  * @param[in]  aInstance         A pointer to an OpenThread instance.
  *
  */
@@ -104,6 +106,8 @@
 /**
  * This function indicates whether there is an ongoing Operation Dataset update request.
  *
+ * Available when `OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE` is enabled.
+ *
  * @param[in]  aInstance         A pointer to an OpenThread instance.
  *
  * @retval TRUE    There is an ongoing update.
diff --git a/include/openthread/diag.h b/include/openthread/diag.h
index 96571f9..98c708e 100644
--- a/include/openthread/diag.h
+++ b/include/openthread/diag.h
@@ -70,8 +70,8 @@
  */
 otError otDiagProcessCmd(otInstance *aInstance,
                          uint8_t     aArgsLength,
-                         char *      aArgs[],
-                         char *      aOutput,
+                         char       *aArgs[],
+                         char       *aOutput,
                          size_t      aOutputMaxLen);
 
 /**
@@ -85,8 +85,13 @@
  * @param[out]  aOutput         The diagnostics execution result.
  * @param[in]   aOutputMaxLen   The output buffer size.
  *
+ * @retval  OT_ERROR_NONE               The command is successfully process.
+ * @retval  OT_ERROR_INVALID_ARGS       The command is supported but invalid arguments provided.
+ * @retval  OT_ERROR_NOT_IMPLEMENTED    The command is not supported.
+ * @retval  OT_ERROR_NO_BUFS            The command string is too long.
+ *
  */
-void otDiagProcessCmdLine(otInstance *aInstance, const char *aString, char *aOutput, size_t aOutputMaxLen);
+otError otDiagProcessCmdLine(otInstance *aInstance, const char *aString, char *aOutput, size_t aOutputMaxLen);
 
 /**
  * This function indicates whether or not the factory diagnostics mode is enabled.
diff --git a/include/openthread/dns.h b/include/openthread/dns.h
index 2a5dd13..22f3fc2 100644
--- a/include/openthread/dns.h
+++ b/include/openthread/dns.h
@@ -89,7 +89,7 @@
      * DNS message.
      *
      */
-    const char *   mKey;
+    const char    *mKey;
     const uint8_t *mValue;       ///< The TXT record value or already encoded TXT-DATA (depending on `mKey`).
     uint16_t       mValueLength; ///< Number of bytes in `mValue` buffer.
 } otDnsTxtEntry;
diff --git a/include/openthread/dns_client.h b/include/openthread/dns_client.h
index 17817c1..c790bc1 100644
--- a/include/openthread/dns_client.h
+++ b/include/openthread/dns_client.h
@@ -81,6 +81,36 @@
 } 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.
+ *
+ */
+typedef enum
+{
+    OT_DNS_TRANSPORT_UNSPECIFIED = 0, /// DNS transport is unspecified.
+    OT_DNS_TRANSPORT_UDP         = 1, /// DNS query should be sent via UDP.
+    OT_DNS_TRANSPORT_TCP         = 2, /// DNS query should be sent via TCP.
+} otDnsTransportProto;
+
+/**
  * This structure represents a DNS query configuration.
  *
  * Any of the fields in this structure can be set to zero to indicate that it is not specified. How the unspecified
@@ -89,11 +119,13 @@
  */
 typedef struct otDnsQueryConfig
 {
-    otSockAddr         mServerSockAddr;  ///< Server address (IPv6 address/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.
+    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;
 
 /**
@@ -203,10 +235,10 @@
  * @retval OT_ERROR_INVALID_STATE Cannot send query since Thread interface is not up.
  *
  */
-otError otDnsClientResolveAddress(otInstance *            aInstance,
-                                  const char *            aHostName,
+otError otDnsClientResolveAddress(otInstance             *aInstance,
+                                  const char             *aHostName,
                                   otDnsAddressCallback    aCallback,
-                                  void *                  aContext,
+                                  void                   *aContext,
                                   const otDnsQueryConfig *aConfig);
 
 /**
@@ -233,10 +265,10 @@
  * @retval OT_ERROR_INVALID_STATE Cannot send query since Thread interface is not up.
  *
  */
-otError otDnsClientResolveIp4Address(otInstance *            aInstance,
-                                     const char *            aHostName,
+otError otDnsClientResolveIp4Address(otInstance             *aInstance,
+                                     const char             *aHostName,
                                      otDnsAddressCallback    aCallback,
-                                     void *                  aContext,
+                                     void                   *aContext,
                                      const otDnsQueryConfig *aConfig);
 
 /**
@@ -253,7 +285,7 @@
  *
  */
 otError otDnsAddressResponseGetHostName(const otDnsAddressResponse *aResponse,
-                                        char *                      aNameBuffer,
+                                        char                       *aNameBuffer,
                                         uint16_t                    aNameBufferSize);
 
 /**
@@ -279,8 +311,8 @@
  */
 otError otDnsAddressResponseGetAddress(const otDnsAddressResponse *aResponse,
                                        uint16_t                    aIndex,
-                                       otIp6Address *              aAddress,
-                                       uint32_t *                  aTtl);
+                                       otIp6Address               *aAddress,
+                                       uint32_t                   *aTtl);
 
 /**
  * This type is an opaque representation of a response to a browse (service instance enumeration) DNS query.
@@ -318,12 +350,13 @@
     uint16_t     mPort;               ///< Service port number.
     uint16_t     mPriority;           ///< Service priority.
     uint16_t     mWeight;             ///< Service weight.
-    char *       mHostNameBuffer;     ///< Buffer to output the service host name (can be NULL if not needed).
+    char        *mHostNameBuffer;     ///< Buffer to output the service host name (can be NULL if not needed).
     uint16_t     mHostNameBufferSize; ///< Size of `mHostNameBuffer`.
     otIp6Address mHostAddress;        ///< The host IPv6 address. Set to all zero if not available.
     uint32_t     mHostAddressTtl;     ///< The host address TTL.
-    uint8_t *    mTxtData;            ///< Buffer to output TXT data (can be NULL if not needed).
-    uint16_t     mTxtDataSize;        ///< On input, size of `mTxtData` buffer. On output `mTxtData` length.
+    uint8_t     *mTxtData;            ///< Buffer to output TXT data (can be NULL if not needed).
+    uint16_t     mTxtDataSize;        ///< On input, size of `mTxtData` buffer. On output number bytes written.
+    bool         mTxtDataTruncated;   ///< Indicates if TXT data could not fit in `mTxtDataSize` and was truncated.
     uint32_t     mTxtDataTtl;         ///< The TXT data TTL.
 } otDnsServiceInfo;
 
@@ -346,10 +379,10 @@
  * @retval OT_ERROR_NO_BUFS     Insufficient buffer to prepare and send query.
  *
  */
-otError otDnsClientBrowse(otInstance *            aInstance,
-                          const char *            aServiceName,
+otError otDnsClientBrowse(otInstance             *aInstance,
+                          const char             *aServiceName,
                           otDnsBrowseCallback     aCallback,
-                          void *                  aContext,
+                          void                   *aContext,
                           const otDnsQueryConfig *aConfig);
 
 /**
@@ -366,7 +399,7 @@
  *
  */
 otError otDnsBrowseResponseGetServiceName(const otDnsBrowseResponse *aResponse,
-                                          char *                     aNameBuffer,
+                                          char                      *aNameBuffer,
                                           uint16_t                   aNameBufferSize);
 
 /**
@@ -393,7 +426,7 @@
  */
 otError otDnsBrowseResponseGetServiceInstance(const otDnsBrowseResponse *aResponse,
                                               uint16_t                   aIndex,
-                                              char *                     aLabelBuffer,
+                                              char                      *aLabelBuffer,
                                               uint8_t                    aLabelBufferSize);
 
 /**
@@ -401,13 +434,15 @@
  *
  * 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.
  * - If no matching AAAA record is found in @p aResponse, `mHostAddress is set to all zero or unspecified address.
  * - If there are multiple AAAA records for the host name in @p aResponse, `mHostAddress` is set to the first one. The
  *   other addresses can be retrieved using `otDnsBrowseResponseGetHostAddress()`.
@@ -423,8 +458,8 @@
  *
  */
 otError otDnsBrowseResponseGetServiceInfo(const otDnsBrowseResponse *aResponse,
-                                          const char *               aInstanceLabel,
-                                          otDnsServiceInfo *         aServiceInfo);
+                                          const char                *aInstanceLabel,
+                                          otDnsServiceInfo          *aServiceInfo);
 
 /**
  * This function gets the host IPv6 address from a DNS browse (service instance enumeration) response.
@@ -448,10 +483,10 @@
  *
  */
 otError otDnsBrowseResponseGetHostAddress(const otDnsBrowseResponse *aResponse,
-                                          const char *               aHostName,
+                                          const char                *aHostName,
                                           uint16_t                   aIndex,
-                                          otIp6Address *             aAddress,
-                                          uint32_t *                 aTtl);
+                                          otIp6Address              *aAddress,
+                                          uint32_t                  *aTtl);
 
 /**
  * This type is an opaque representation of a response to a service instance resolution DNS query.
@@ -488,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).
@@ -500,11 +547,11 @@
  * @retval OT_ERROR_INVALID_ARGS  @p aInstanceLabel is NULL.
  *
  */
-otError otDnsClientResolveService(otInstance *            aInstance,
-                                  const char *            aInstanceLabel,
-                                  const char *            aServiceName,
+otError otDnsClientResolveService(otInstance             *aInstance,
+                                  const char             *aInstanceLabel,
+                                  const char             *aServiceName,
                                   otDnsServiceCallback    aCallback,
-                                  void *                  aContext,
+                                  void                   *aContext,
                                   const otDnsQueryConfig *aConfig);
 
 /**
@@ -524,9 +571,9 @@
  *
  */
 otError otDnsServiceResponseGetServiceName(const otDnsServiceResponse *aResponse,
-                                           char *                      aLabelBuffer,
+                                           char                       *aLabelBuffer,
                                            uint8_t                     aLabelBufferSize,
-                                           char *                      aNameBuffer,
+                                           char                       *aNameBuffer,
                                            uint16_t                    aNameBufferSize);
 
 /**
@@ -534,9 +581,18 @@
  *
  * This function MUST only be used from `otDnsServiceCallback`.
  *
- * - 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.
+ * A service resolution DNS response may include AAAA records in its Additional Data section for 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 retrieve AAAA record(s) if included in the response. If it
+ * is not included `mHostAddress` is set to all zero (unspecified address). If the caller wants to resolve the host
+ * address it can call `otDnsClientResolveAddress()` with the host name to start an address resolution query.
+ *
+ * - 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.
  * - If there are multiple AAAA records for the host name in @p aResponse, `mHostAddress` is set to the first one. The
  *   other addresses can be retrieved using `otDnsServiceResponseGetHostAddress()`.
@@ -545,7 +601,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.
  *
@@ -574,10 +630,10 @@
  *
  */
 otError otDnsServiceResponseGetHostAddress(const otDnsServiceResponse *aResponse,
-                                           const char *                aHostName,
+                                           const char                 *aHostName,
                                            uint16_t                    aIndex,
-                                           otIp6Address *              aAddress,
-                                           uint32_t *                  aTtl);
+                                           otIp6Address               *aAddress,
+                                           uint32_t                   *aTtl);
 
 /**
  * @}
diff --git a/include/openthread/dnssd_server.h b/include/openthread/dnssd_server.h
index 7366698..cd6463c 100644
--- a/include/openthread/dnssd_server.h
+++ b/include/openthread/dnssd_server.h
@@ -112,15 +112,15 @@
  */
 typedef struct otDnssdServiceInstanceInfo
 {
-    const char *        mFullName;   ///< Full instance name (e.g. "OpenThread._ipps._tcp.default.service.arpa.").
-    const char *        mHostName;   ///< Host name (e.g. "ot-host.default.service.arpa.").
+    const char         *mFullName;   ///< Full instance name (e.g. "OpenThread._ipps._tcp.default.service.arpa.").
+    const char         *mHostName;   ///< Host name (e.g. "ot-host.default.service.arpa.").
     uint8_t             mAddressNum; ///< Number of host IPv6 addresses.
     const otIp6Address *mAddresses;  ///< Host IPv6 addresses.
     uint16_t            mPort;       ///< Service port.
     uint16_t            mPriority;   ///< Service priority.
     uint16_t            mWeight;     ///< Service weight.
     uint16_t            mTxtLength;  ///< Service TXT RDATA length.
-    const uint8_t *     mTxtData;    ///< Service TXT RDATA.
+    const uint8_t      *mTxtData;    ///< Service TXT RDATA.
     uint32_t            mTtl;        ///< Service TTL (in seconds).
 } otDnssdServiceInstanceInfo;
 
@@ -177,10 +177,10 @@
  * @param[in] aContext      A pointer to the application-specific context.
  *
  */
-void otDnssdQuerySetCallbacks(otInstance *                    aInstance,
+void otDnssdQuerySetCallbacks(otInstance                     *aInstance,
                               otDnssdQuerySubscribeCallback   aSubscribe,
                               otDnssdQueryUnsubscribeCallback aUnsubscribe,
-                              void *                          aContext);
+                              void                           *aContext);
 
 /**
  * This function notifies a discovered service instance.
@@ -195,8 +195,8 @@
  * @param[in] aInstanceInfo     A pointer to the discovered service instance information.
  *
  */
-void otDnssdQueryHandleDiscoveredServiceInstance(otInstance *                aInstance,
-                                                 const char *                aServiceFullName,
+void otDnssdQueryHandleDiscoveredServiceInstance(otInstance                 *aInstance,
+                                                 const char                 *aServiceFullName,
                                                  otDnssdServiceInstanceInfo *aInstanceInfo);
 /**
  * This function notifies a discovered host.
@@ -246,6 +246,35 @@
 const otDnssdCounters *otDnssdGetCounters(otInstance *aInstance);
 
 /**
+ * Enable or disable forwarding DNS queries to platform DNS upstream API.
+ *
+ * Available when `OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE` is enabled.
+ *
+ * @param[in]  aInstance  A pointer to an OpenThread instance.
+ * @param[in]  aEnabled   A boolean to enable/disable forwarding DNS queries to upstream.
+ *
+ * @sa otPlatDnsStartUpstreamQuery
+ * @sa otPlatDnsCancelUpstreamQuery
+ * @sa otPlatDnsUpstreamQueryDone
+ *
+ */
+void otDnssdUpstreamQuerySetEnabled(otInstance *aInstance, bool aEnabled);
+
+/**
+ * Returns whether the DNSSD server will forward DNS queries to the platform DNS upstream API.
+ *
+ * Available when `OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE` is enabled.
+ *
+ * @param[in]  aInstance  A pointer to an OpenThread instance.
+ * @retval     TRUE       If the DNSSD server will forward DNS queries.
+ * @retval     FALSE      If the DNSSD server will not forward DNS queries.
+ *
+ * @sa otDnssdUpstreamQuerySetEnabled
+ *
+ */
+bool otDnssdUpstreamQueryIsEnabled(otInstance *aInstance);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/history_tracker.h b/include/openthread/history_tracker.h
index a8dc7c3..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).
@@ -193,6 +193,35 @@
 } otHistoryTrackerNeighborInfo;
 
 /**
+ * This enumeration defines the events in a router info (i.e. whether router is added, removed, or changed).
+ *
+ */
+typedef enum
+{
+    OT_HISTORY_TRACKER_ROUTER_EVENT_ADDED            = 0, ///< Router is added (router ID allocated).
+    OT_HISTORY_TRACKER_ROUTER_EVENT_REMOVED          = 1, ///< Router entry is removed (router ID released).
+    OT_HISTORY_TRACKER_ROUTER_EVENT_NEXT_HOP_CHANGED = 2, ///< Router entry next hop and cost changed.
+    OT_HISTORY_TRACKER_ROUTER_EVENT_COST_CHANGED     = 3, ///< Router entry path cost changed (next hop as before).
+} otHistoryTrackerRouterEvent;
+
+#define OT_HISTORY_TRACKER_NO_NEXT_HOP 63 ///< No next hop - For `mNextHop` in `otHistoryTrackerRouterInfo`.
+
+#define OT_HISTORY_TRACKER_INFINITE_PATH_COST 0 ///< Infinite path cost - used in `otHistoryTrackerRouterInfo`.
+
+/**
+ * This structure represents a router table entry event.
+ *
+ */
+typedef struct otHistoryTrackerRouterInfo
+{
+    uint8_t mEvent : 2;       ///< Router entry event (`OT_HISTORY_TRACKER_ROUTER_EVENT_*` enumeration).
+    uint8_t mRouterId : 6;    ///< Router ID.
+    uint8_t mNextHop;         ///< Next Hop Router ID - `OT_HISTORY_TRACKER_NO_NEXT_HOP` if no next hop.
+    uint8_t mOldPathCost : 4; ///< Old path cost - `OT_HISTORY_TRACKER_INFINITE_PATH_COST` if infinite or unknown.
+    uint8_t mPathCost : 4;    ///< New path cost - `OT_HISTORY_TRACKER_INFINITE_PATH_COST` if infinite or unknown.
+} otHistoryTrackerRouterInfo;
+
+/**
  * This enumeration defines the events for a Network Data entry (i.e., whether an entry is added or removed).
  *
  */
@@ -251,9 +280,9 @@
  * @returns A pointer to `otHistoryTrackerNetworkInfo` entry or `NULL` if no more entries in the list.
  *
  */
-const otHistoryTrackerNetworkInfo *otHistoryTrackerIterateNetInfoHistory(otInstance *              aInstance,
+const otHistoryTrackerNetworkInfo *otHistoryTrackerIterateNetInfoHistory(otInstance               *aInstance,
                                                                          otHistoryTrackerIterator *aIterator,
-                                                                         uint32_t *                aEntryAge);
+                                                                         uint32_t                 *aEntryAge);
 
 /**
  * This function iterates over the entries in the unicast address history list.
@@ -269,9 +298,9 @@
  *
  */
 const otHistoryTrackerUnicastAddressInfo *otHistoryTrackerIterateUnicastAddressHistory(
-    otInstance *              aInstance,
+    otInstance               *aInstance,
     otHistoryTrackerIterator *aIterator,
-    uint32_t *                aEntryAge);
+    uint32_t                 *aEntryAge);
 
 /**
  * This function iterates over the entries in the multicast address history list.
@@ -287,9 +316,9 @@
  *
  */
 const otHistoryTrackerMulticastAddressInfo *otHistoryTrackerIterateMulticastAddressHistory(
-    otInstance *              aInstance,
+    otInstance               *aInstance,
     otHistoryTrackerIterator *aIterator,
-    uint32_t *                aEntryAge);
+    uint32_t                 *aEntryAge);
 
 /**
  * This function iterates over the entries in the RX message history list.
@@ -304,9 +333,9 @@
  * @returns The `otHistoryTrackerMessageInfo` entry or `NULL` if no more entries in the list.
  *
  */
-const otHistoryTrackerMessageInfo *otHistoryTrackerIterateRxHistory(otInstance *              aInstance,
+const otHistoryTrackerMessageInfo *otHistoryTrackerIterateRxHistory(otInstance               *aInstance,
                                                                     otHistoryTrackerIterator *aIterator,
-                                                                    uint32_t *                aEntryAge);
+                                                                    uint32_t                 *aEntryAge);
 
 /**
  * This function iterates over the entries in the TX message history list.
@@ -321,9 +350,9 @@
  * @returns The `otHistoryTrackerMessageInfo` entry or `NULL` if no more entries in the list.
  *
  */
-const otHistoryTrackerMessageInfo *otHistoryTrackerIterateTxHistory(otInstance *              aInstance,
+const otHistoryTrackerMessageInfo *otHistoryTrackerIterateTxHistory(otInstance               *aInstance,
                                                                     otHistoryTrackerIterator *aIterator,
-                                                                    uint32_t *                aEntryAge);
+                                                                    uint32_t                 *aEntryAge);
 
 /**
  * This function iterates over the entries in the neighbor history list.
@@ -338,9 +367,26 @@
  * @returns The `otHistoryTrackerNeighborInfo` entry or `NULL` if no more entries in the list.
  *
  */
-const otHistoryTrackerNeighborInfo *otHistoryTrackerIterateNeighborHistory(otInstance *              aInstance,
+const otHistoryTrackerNeighborInfo *otHistoryTrackerIterateNeighborHistory(otInstance               *aInstance,
                                                                            otHistoryTrackerIterator *aIterator,
-                                                                           uint32_t *                aEntryAge);
+                                                                           uint32_t                 *aEntryAge);
+
+/**
+ * This function iterates over the entries in the router history list.
+ *
+ * @param[in]     aInstance  A pointer to the OpenThread instance.
+ * @param[in,out] aIterator  A pointer to an iterator. MUST be initialized or the behavior is undefined.
+ * @param[out]    aEntryAge  A pointer to a variable to output the entry's age. MUST NOT be NULL.
+ *                           Age is provided as the duration (in milliseconds) from when entry was recorded to
+ *                           @p aIterator initialization time. It is set to `OT_HISTORY_TRACKER_MAX_AGE` for entries
+ *                           older than max age.
+ *
+ * @returns The `otHistoryTrackerRouterInfo` entry or `NULL` if no more entries in the list.
+ *
+ */
+const otHistoryTrackerRouterInfo *otHistoryTrackerIterateRouterHistory(otInstance               *aInstance,
+                                                                       otHistoryTrackerIterator *aIterator,
+                                                                       uint32_t                 *aEntryAge);
 
 /**
  * This function iterates over the entries in the Network Data on mesh prefix entry history list.
@@ -355,9 +401,9 @@
  * @returns The `otHistoryTrackerOnMeshPrefixInfo` entry or `NULL` if no more entries in the list.
  *
  */
-const otHistoryTrackerOnMeshPrefixInfo *otHistoryTrackerIterateOnMeshPrefixHistory(otInstance *              aInstance,
+const otHistoryTrackerOnMeshPrefixInfo *otHistoryTrackerIterateOnMeshPrefixHistory(otInstance               *aInstance,
                                                                                    otHistoryTrackerIterator *aIterator,
-                                                                                   uint32_t *                aEntryAge);
+                                                                                   uint32_t                 *aEntryAge);
 
 /**
  * This function iterates over the entries in the Network Data external route entry history list.
@@ -373,9 +419,9 @@
  *
  */
 const otHistoryTrackerExternalRouteInfo *otHistoryTrackerIterateExternalRouteHistory(
-    otInstance *              aInstance,
+    otInstance               *aInstance,
     otHistoryTrackerIterator *aIterator,
-    uint32_t *                aEntryAge);
+    uint32_t                 *aEntryAge);
 
 /**
  * This function converts a given entry age to a human-readable string.
diff --git a/include/openthread/icmp6.h b/include/openthread/icmp6.h
index cefe596..38bd6a6 100644
--- a/include/openthread/icmp6.h
+++ b/include/openthread/icmp6.h
@@ -66,6 +66,8 @@
     OT_ICMP6_TYPE_ECHO_REPLY        = 129, ///< Echo Reply
     OT_ICMP6_TYPE_ROUTER_SOLICIT    = 133, ///< Router Solicitation
     OT_ICMP6_TYPE_ROUTER_ADVERT     = 134, ///< Router Advertisement
+    OT_ICMP6_TYPE_NEIGHBOR_SOLICIT  = 135, ///< Neighbor Solicitation
+    OT_ICMP6_TYPE_NEIGHBOR_ADVERT   = 136, ///< Neighbor Advertisement
 } otIcmp6Type;
 
 /**
@@ -115,8 +117,8 @@
  * @param[in]  aIcmpHeader   A pointer to the received ICMPv6 header.
  *
  */
-typedef void (*otIcmp6ReceiveCallback)(void *               aContext,
-                                       otMessage *          aMessage,
+typedef void (*otIcmp6ReceiveCallback)(void                *aContext,
+                                       otMessage           *aMessage,
                                        const otMessageInfo *aMessageInfo,
                                        const otIcmp6Header *aIcmpHeader);
 
@@ -127,7 +129,7 @@
 typedef struct otIcmp6Handler
 {
     otIcmp6ReceiveCallback mReceiveCallback; ///< The ICMPv6 received callback
-    void *                 mContext;         ///< A pointer to arbitrary context information.
+    void                  *mContext;         ///< A pointer to arbitrary context information.
     struct otIcmp6Handler *mNext;            ///< A pointer to the next handler in the list.
 } otIcmp6Handler;
 
@@ -188,8 +190,8 @@
  *                           May be zero.
  *
  */
-otError otIcmp6SendEchoRequest(otInstance *         aInstance,
-                               otMessage *          aMessage,
+otError otIcmp6SendEchoRequest(otInstance          *aInstance,
+                               otMessage           *aMessage,
                                const otMessageInfo *aMessageInfo,
                                uint16_t             aIdentifier);
 
diff --git a/include/openthread/instance.h b/include/openthread/instance.h
index db89b62..616a1d9 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 (230)
+#define OPENTHREAD_API_VERSION (321)
 
 /**
  * @addtogroup api-instance
@@ -196,6 +196,7 @@
     OT_CHANGED_JOINER_STATE                 = 1 << 27, ///< Joiner state changed
     OT_CHANGED_ACTIVE_DATASET               = 1 << 28, ///< Active Operational Dataset changed
     OT_CHANGED_PENDING_DATASET              = 1 << 29, ///< Pending Operational Dataset changed
+    OT_CHANGED_NAT64_TRANSLATOR_STATE       = 1 << 30, ///< The state of NAT64 translator changed
 };
 
 /**
@@ -250,7 +251,7 @@
 void otInstanceReset(otInstance *aInstance);
 
 /**
- * This method deletes all the settings stored on non-volatile memory, and then triggers platform reset.
+ * Deletes all the settings stored on non-volatile memory, and then triggers a platform reset.
  *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  *
diff --git a/include/openthread/ip6.h b/include/openthread/ip6.h
index f624b42..c04bdc2 100644
--- a/include/openthread/ip6.h
+++ b/include/openthread/ip6.h
@@ -229,7 +229,7 @@
     otIp6Address mPeerAddr; ///< The peer IPv6 address.
     uint16_t     mSockPort; ///< The local transport-layer port.
     uint16_t     mPeerPort; ///< The peer transport-layer port.
-    const void * mLinkInfo; ///< A pointer to link-specific information.
+    const void  *mLinkInfo; ///< A pointer to link-specific information.
     uint8_t      mHopLimit; ///< The IPv6 Hop Limit value. Only applies if `mAllowZeroHopLimit` is FALSE.
                             ///< If `0`, IPv6 Hop Limit is default value `OPENTHREAD_CONFIG_IP6_HOP_LIMIT_DEFAULT`.
                             ///< Otherwise, specifies the IPv6 Hop Limit.
@@ -257,9 +257,9 @@
 };
 
 /**
- * This function brings up/down the IPv6 interface.
+ * Brings the IPv6 interface up or down.
  *
- * Call this function to enable/disable IPv6 communication.
+ * Call this to enable or disable IPv6 communication.
  *
  * @param[in] aInstance A pointer to an OpenThread instance.
  * @param[in] aEnabled  TRUE to enable IPv6, FALSE otherwise.
@@ -272,7 +272,7 @@
 otError otIp6SetEnabled(otInstance *aInstance, bool aEnabled);
 
 /**
- * This function indicates whether or not the IPv6 interface is up.
+ * Indicates whether or not the IPv6 interface is up.
  *
  * @param[in] aInstance A pointer to an OpenThread instance.
  *
@@ -283,10 +283,10 @@
 bool otIp6IsEnabled(otInstance *aInstance);
 
 /**
- * Add a Network Interface Address to the Thread interface.
+ * Adds a Network Interface Address to the Thread interface.
  *
  * The passed-in instance @p aAddress is copied by the Thread interface. The Thread interface only
- * supports a fixed number of externally added unicast addresses. See OPENTHREAD_CONFIG_IP6_MAX_EXT_UCAST_ADDRS.
+ * supports a fixed number of externally added unicast addresses. See `OPENTHREAD_CONFIG_IP6_MAX_EXT_UCAST_ADDRS`.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  * @param[in]  aAddress  A pointer to a Network Interface Address.
@@ -298,7 +298,7 @@
 otError otIp6AddUnicastAddress(otInstance *aInstance, const otNetifAddress *aAddress);
 
 /**
- * Remove a Network Interface Address from the Thread interface.
+ * Removes a Network Interface Address from the Thread interface.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  * @param[in]  aAddress  A pointer to an IP Address.
@@ -310,7 +310,7 @@
 otError otIp6RemoveUnicastAddress(otInstance *aInstance, const otIp6Address *aAddress);
 
 /**
- * Get the list of IPv6 addresses assigned to the Thread interface.
+ * Gets the list of IPv6 addresses assigned to the Thread interface.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -319,10 +319,10 @@
 const otNetifAddress *otIp6GetUnicastAddresses(otInstance *aInstance);
 
 /**
- * Subscribe the Thread interface to a Network Interface Multicast Address.
+ * Subscribes the Thread interface to a Network Interface Multicast Address.
  *
  * The passed in instance @p aAddress will be copied by the Thread interface. The Thread interface only
- * supports a fixed number of externally added multicast addresses. See OPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS.
+ * supports a fixed number of externally added multicast addresses. See `OPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS`.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  * @param[in]  aAddress  A pointer to an IP Address.
@@ -338,7 +338,7 @@
 otError otIp6SubscribeMulticastAddress(otInstance *aInstance, const otIp6Address *aAddress);
 
 /**
- * Unsubscribe the Thread interface to a Network Interface Multicast Address.
+ * Unsubscribes the Thread interface to a Network Interface Multicast Address.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  * @param[in]  aAddress  A pointer to an IP Address.
@@ -351,7 +351,7 @@
 otError otIp6UnsubscribeMulticastAddress(otInstance *aInstance, const otIp6Address *aAddress);
 
 /**
- * Get the list of IPv6 multicast addresses subscribed to the Thread interface.
+ * Gets the list of IPv6 multicast addresses subscribed to the Thread interface.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -361,7 +361,7 @@
 const otNetifMulticastAddress *otIp6GetMulticastAddresses(otInstance *aInstance);
 
 /**
- * Check if multicast promiscuous mode is enabled on the Thread interface.
+ * Checks if multicast promiscuous mode is enabled on the Thread interface.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -371,7 +371,7 @@
 bool otIp6IsMulticastPromiscuousEnabled(otInstance *aInstance);
 
 /**
- * Enable multicast promiscuous mode on the Thread interface.
+ * Enables or disables multicast promiscuous mode on the Thread interface.
  *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  * @param[in]  aEnabled   TRUE to enable Multicast Promiscuous mode, FALSE otherwise.
@@ -414,8 +414,8 @@
  * @sa otMessageFree
  *
  */
-otMessage *otIp6NewMessageFromBuffer(otInstance *             aInstance,
-                                     const uint8_t *          aData,
+otMessage *otIp6NewMessageFromBuffer(otInstance              *aInstance,
+                                     const uint8_t           *aData,
                                      uint16_t                 aDataLength,
                                      const otMessageSettings *aSettings);
 
@@ -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.
 
 /**
@@ -638,8 +653,9 @@
 /**
  * This function converts a given IPv6 socket address to a human-readable string.
  *
- * The IPv6 socket address string is formatted as "[<address>]:<port>" where `<address> is shown as 16 hex values
- * separated by ':' and `<port>` is the port number in decimal format (i.e., "[%x:%x:...:%x]:%u")
+ * The IPv6 socket address string is formatted as [`address`]:`port` where `address` is shown
+ * as 16 hex values separated by `:` and `port` is the port number in decimal format,
+ * for example "[%x:%x:...:%x]:%u".
  *
  * 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.
@@ -680,6 +696,16 @@
 uint8_t otIp6PrefixMatch(const otIp6Address *aFirst, const otIp6Address *aSecond);
 
 /**
+ * This method gets a prefix with @p aLength from @p aAddress.
+ *
+ * @param[in]  aAddress   A pointer to an IPv6 address.
+ * @param[in]  aLength    The length of prefix in bits.
+ * @param[out] aPrefix    A pointer to output the IPv6 prefix.
+ *
+ */
+void otIp6GetPrefix(const otIp6Address *aAddress, uint8_t aLength, otIp6Prefix *aPrefix);
+
+/**
  * This function indicates whether or not a given IPv6 address is the Unspecified Address.
  *
  * @param[in]  aAddress   A pointer to an IPv6 address.
@@ -775,7 +801,7 @@
  * @sa otIp6RegisterMulticastListeners
  *
  */
-typedef void (*otIp6RegisterMulticastListenersCallback)(void *              aContext,
+typedef void (*otIp6RegisterMulticastListenersCallback)(void               *aContext,
                                                         otError             aError,
                                                         uint8_t             aMlrStatus,
                                                         const otIp6Address *aFailedAddresses,
@@ -809,12 +835,12 @@
  * @sa otIp6RegisterMulticastListenersCallback
  *
  */
-otError otIp6RegisterMulticastListeners(otInstance *                            aInstance,
-                                        const otIp6Address *                    aAddresses,
+otError otIp6RegisterMulticastListeners(otInstance                             *aInstance,
+                                        const otIp6Address                     *aAddresses,
                                         uint8_t                                 aAddressNum,
-                                        const uint32_t *                        aTimeout,
+                                        const uint32_t                         *aTimeout,
                                         otIp6RegisterMulticastListenersCallback aCallback,
-                                        void *                                  aContext);
+                                        void                                   *aContext);
 
 /**
  * This function sets the Mesh Local IID (for test purpose).
@@ -841,6 +867,54 @@
 const char *otIp6ProtoToString(uint8_t aIpProto);
 
 /**
+ * This structure represents the counters for packets and bytes.
+ *
+ */
+typedef struct otPacketsAndBytes
+{
+    uint64_t mPackets; ///< The number of packets.
+    uint64_t mBytes;   ///< The number of bytes.
+} otPacketsAndBytes;
+
+/**
+ * This structure represents the counters of packets forwarded via Border Routing.
+ *
+ */
+typedef struct otBorderRoutingCounters
+{
+    otPacketsAndBytes mInboundUnicast;    ///< The counters for inbound unicast.
+    otPacketsAndBytes mInboundMulticast;  ///< The counters for inbound multicast.
+    otPacketsAndBytes mOutboundUnicast;   ///< The counters for outbound unicast.
+    otPacketsAndBytes mOutboundMulticast; ///< The counters for outbound multicast.
+    uint32_t          mRaRx;              ///< The number of received RA packets.
+    uint32_t          mRaTxSuccess;       ///< The number of RA packets successfully transmitted.
+    uint32_t          mRaTxFailure;       ///< The number of RA packets failed to transmit.
+    uint32_t          mRsRx;              ///< The number of received RS packets.
+    uint32_t          mRsTxSuccess;       ///< The number of RS packets successfully transmitted.
+    uint32_t          mRsTxFailure;       ///< The number of RS packets failed to transmit.
+} otBorderRoutingCounters;
+
+/**
+ * Gets the Border Routing counters.
+ *
+ * This function requires the build-time feature `OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE` to be enabled.
+ *
+ * @param[in]  aInstance  A pointer to an OpenThread instance.
+ *
+ * @returns A pointer to the Border Routing counters.
+ *
+ */
+const otBorderRoutingCounters *otIp6GetBorderRoutingCounters(otInstance *aInstance);
+
+/**
+ * Resets the Border Routing counters.
+ *
+ * @param[in]  aInstance  A pointer to an OpenThread instance.
+ *
+ */
+void otIp6ResetBorderRoutingCounters(otInstance *aInstance);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/joiner.h b/include/openthread/joiner.h
index a1873f7..586e88f 100644
--- a/include/openthread/joiner.h
+++ b/include/openthread/joiner.h
@@ -112,15 +112,15 @@
  * @retval OT_ERROR_INVALID_STATE     The IPv6 stack is not enabled or Thread stack is fully enabled.
  *
  */
-otError otJoinerStart(otInstance *     aInstance,
-                      const char *     aPskd,
-                      const char *     aProvisioningUrl,
-                      const char *     aVendorName,
-                      const char *     aVendorModel,
-                      const char *     aVendorSwVersion,
-                      const char *     aVendorData,
+otError otJoinerStart(otInstance      *aInstance,
+                      const char      *aPskd,
+                      const char      *aProvisioningUrl,
+                      const char      *aVendorName,
+                      const char      *aVendorModel,
+                      const char      *aVendorSwVersion,
+                      const char      *aVendorData,
                       otJoinerCallback aCallback,
-                      void *           aContext);
+                      void            *aContext);
 
 /**
  * Disables the Thread Joiner role.
diff --git a/include/openthread/link.h b/include/openthread/link.h
index 0a7280c..289cf85 100644
--- a/include/openthread/link.h
+++ b/include/openthread/link.h
@@ -52,7 +52,7 @@
  * @{
  *
  */
-#define OT_US_PER_TEN_SYMBOLS 160 ///< The microseconds per 10 symbols.
+#define OT_US_PER_TEN_SYMBOLS OT_RADIO_TEN_SYMBOLS_TIME ///< Time for 10 symbols in units of microseconds
 
 /**
  * This structure represents link-specific information for messages received from the Thread radio.
@@ -430,11 +430,11 @@
  * @retval OT_ERROR_BUSY  Already performing an Active Scan.
  *
  */
-otError otLinkActiveScan(otInstance *             aInstance,
+otError otLinkActiveScan(otInstance              *aInstance,
                          uint32_t                 aScanChannels,
                          uint16_t                 aScanDuration,
                          otHandleActiveScanResult aCallback,
-                         void *                   aCallbackContext);
+                         void                    *aCallbackContext);
 
 /**
  * This function indicates whether or not an IEEE 802.15.4 Active Scan is currently in progress.
@@ -468,11 +468,11 @@
  * @retval OT_ERROR_BUSY  Could not start the energy scan.
  *
  */
-otError otLinkEnergyScan(otInstance *             aInstance,
+otError otLinkEnergyScan(otInstance              *aInstance,
                          uint32_t                 aScanChannels,
                          uint16_t                 aScanDuration,
                          otHandleEnergyScanResult aCallback,
-                         void *                   aCallbackContext);
+                         void                    *aCallbackContext);
 
 /**
  * This function indicates whether or not an IEEE 802.15.4 Energy Scan is currently in progress.
@@ -565,7 +565,7 @@
 otError otLinkSetSupportedChannelMask(otInstance *aInstance, uint32_t aChannelMask);
 
 /**
- * Get the IEEE 802.15.4 Extended Address.
+ * Gets the IEEE 802.15.4 Extended Address.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -575,9 +575,9 @@
 const otExtAddress *otLinkGetExtendedAddress(otInstance *aInstance);
 
 /**
- * This function sets the IEEE 802.15.4 Extended Address.
+ * Sets the IEEE 802.15.4 Extended Address.
  *
- * This function succeeds only when Thread protocols are disabled.
+ * @note Only succeeds when Thread protocols are disabled.
  *
  * @param[in]  aInstance    A pointer to an OpenThread instance.
  * @param[in]  aExtAddress  A pointer to the IEEE 802.15.4 Extended Address.
@@ -965,7 +965,7 @@
 const otMacCounters *otLinkGetCounters(otInstance *aInstance);
 
 /**
- * Reset the MAC layer counters.
+ * Resets the MAC layer counters.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -1034,7 +1034,7 @@
 uint8_t otLinkCslGetChannel(otInstance *aInstance);
 
 /**
- * This function sets the CSL channel.
+ * Sets the CSL channel.
  *
  * @param[in]  aInstance      A pointer to an OpenThread instance.
  * @param[in]  aChannel       The CSL sample channel. Channel value should be `0` (Set CSL Channel unspecified) or
@@ -1057,7 +1057,7 @@
 uint16_t otLinkCslGetPeriod(otInstance *aInstance);
 
 /**
- * This function sets the CSL period.
+ * Sets the CSL period in units of 10 symbols. Disable CSL by setting this parameter to `0`.
  *
  * @param[in]  aInstance      A pointer to an OpenThread instance.
  * @param[in]  aPeriod        The CSL period in units of 10 symbols.
@@ -1079,7 +1079,7 @@
 uint32_t otLinkCslGetTimeout(otInstance *aInstance);
 
 /**
- * This function sets the CSL timeout.
+ * Sets the CSL timeout in seconds.
  *
  * @param[in]  aInstance      A pointer to an OpenThread instance.
  * @param[in]  aTimeout       The CSL timeout in seconds.
diff --git a/include/openthread/link_metrics.h b/include/openthread/link_metrics.h
index 0b039b5..fc9a7f0 100644
--- a/include/openthread/link_metrics.h
+++ b/include/openthread/link_metrics.h
@@ -116,10 +116,10 @@
  * @param[in]  aContext        A pointer to application-specific context.
  *
  */
-typedef void (*otLinkMetricsReportCallback)(const otIp6Address *       aSource,
+typedef void (*otLinkMetricsReportCallback)(const otIp6Address        *aSource,
                                             const otLinkMetricsValues *aMetricsValues,
                                             uint8_t                    aStatus,
-                                            void *                     aContext);
+                                            void                      *aContext);
 /**
  * This function pointer is called when a Link Metrics Management Response is received.
  *
@@ -140,9 +140,9 @@
  *
  */
 typedef void (*otLinkMetricsEnhAckProbingIeReportCallback)(otShortAddress             aShortAddress,
-                                                           const otExtAddress *       aExtAddress,
+                                                           const otExtAddress        *aExtAddress,
                                                            const otLinkMetricsValues *aMetricsValues,
-                                                           void *                     aContext);
+                                                           void                      *aContext);
 
 /**
  * This function sends an MLE Data Request to query Link Metrics.
@@ -162,15 +162,15 @@
  * @retval OT_ERROR_NOT_CAPABLE       The neighbor is not a Thread 1.2 device and does not support Link Metrics.
  *
  */
-otError otLinkMetricsQuery(otInstance *                aInstance,
-                           const otIp6Address *        aDestination,
+otError otLinkMetricsQuery(otInstance                 *aInstance,
+                           const otIp6Address         *aDestination,
                            uint8_t                     aSeriesId,
-                           const otLinkMetrics *       aLinkMetricsFlags,
+                           const otLinkMetrics        *aLinkMetricsFlags,
                            otLinkMetricsReportCallback aCallback,
-                           void *                      aCallbackContext);
+                           void                       *aCallbackContext);
 
 /**
- * This function sends an MLE Link Metrics Management Request to configure/clear a Forward Tracking Series.
+ * Sends an MLE Link Metrics Management Request to configure or clear a Forward Tracking Series.
  *
  * @param[in] aInstance          A pointer to an OpenThread instance.
  * @param[in] aDestination       A pointer to the destination address.
@@ -189,13 +189,13 @@
  * @retval OT_ERROR_NOT_CAPABLE       The neighbor is not a Thread 1.2 device and does not support Link Metrics.
  *
  */
-otError otLinkMetricsConfigForwardTrackingSeries(otInstance *                      aInstance,
-                                                 const otIp6Address *              aDestination,
+otError otLinkMetricsConfigForwardTrackingSeries(otInstance                       *aInstance,
+                                                 const otIp6Address               *aDestination,
                                                  uint8_t                           aSeriesId,
                                                  otLinkMetricsSeriesFlags          aSeriesFlags,
-                                                 const otLinkMetrics *             aLinkMetricsFlags,
+                                                 const otLinkMetrics              *aLinkMetricsFlags,
                                                  otLinkMetricsMgmtResponseCallback aCallback,
-                                                 void *                            aCallbackContext);
+                                                 void                             *aCallbackContext);
 
 /**
  * This function sends an MLE Link Metrics Management Request to configure/clear an Enhanced-ACK Based Probing.
@@ -218,17 +218,17 @@
  * @retval OT_ERROR_NOT_CAPABLE       The neighbor is not a Thread 1.2 device and does not support Link Metrics.
  *
  */
-otError otLinkMetricsConfigEnhAckProbing(otInstance *                               aInstance,
-                                         const otIp6Address *                       aDestination,
+otError otLinkMetricsConfigEnhAckProbing(otInstance                                *aInstance,
+                                         const otIp6Address                        *aDestination,
                                          otLinkMetricsEnhAckFlags                   aEnhAckFlags,
-                                         const otLinkMetrics *                      aLinkMetricsFlags,
+                                         const otLinkMetrics                       *aLinkMetricsFlags,
                                          otLinkMetricsMgmtResponseCallback          aCallback,
-                                         void *                                     aCallbackContext,
+                                         void                                      *aCallbackContext,
                                          otLinkMetricsEnhAckProbingIeReportCallback aEnhAckCallback,
-                                         void *                                     aEnhAckCallbackContext);
+                                         void                                      *aEnhAckCallbackContext);
 
 /**
- * This function sends an MLE Link Probe message.
+ * Sends an MLE Link Probe message.
  *
  * @param[in] aInstance       A pointer to an OpenThread instance.
  * @param[in] aDestination    A pointer to the destination address.
@@ -242,7 +242,7 @@
  * @retval OT_ERROR_NOT_CAPABLE       The neighbor is not a Thread 1.2 device and does not support Link Metrics.
  *
  */
-otError otLinkMetricsSendLinkProbe(otInstance *        aInstance,
+otError otLinkMetricsSendLinkProbe(otInstance         *aInstance,
                                    const otIp6Address *aDestination,
                                    uint8_t             aSeriesId,
                                    uint8_t             aLength);
diff --git a/include/openthread/link_raw.h b/include/openthread/link_raw.h
index 02293e9..1f43a05 100644
--- a/include/openthread/link_raw.h
+++ b/include/openthread/link_raw.h
@@ -174,7 +174,7 @@
  *                              OT_ERROR_ABORT when transmission was aborted for other reasons.
  *
  */
-typedef void (*otLinkRawTransmitDone)(otInstance *  aInstance,
+typedef void (*otLinkRawTransmitDone)(otInstance   *aInstance,
                                       otRadioFrame *aFrame,
                                       otRadioFrame *aAckFrame,
                                       otError       aError);
@@ -236,12 +236,12 @@
  * @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.
  *
  */
-otError otLinkRawEnergyScan(otInstance *            aInstance,
+otError otLinkRawEnergyScan(otInstance             *aInstance,
                             uint8_t                 aScanChannel,
                             uint16_t                aScanDuration,
                             otLinkRawEnergyScanDone aCallback);
@@ -346,7 +346,7 @@
  * @retval OT_ERROR_INVALID_STATE    If the raw link-layer isn't enabled.
  *
  */
-otError otLinkRawSetMacKey(otInstance *    aInstance,
+otError otLinkRawSetMacKey(otInstance     *aInstance,
                            uint8_t         aKeyIdMode,
                            uint8_t         aKeyId,
                            const otMacKey *aPrevKey,
@@ -356,6 +356,9 @@
 /**
  * Sets the current MAC frame counter value.
  *
+ * This function always sets the MAC counter to the new given value @p aMacFrameCounter independent of the current
+ * value.
+ *
  * @param[in]   aInstance         A pointer to an OpenThread instance.
  * @param[in]   aMacFrameCounter  The MAC frame counter value.
  *
@@ -366,6 +369,18 @@
 otError otLinkRawSetMacFrameCounter(otInstance *aInstance, uint32_t aMacFrameCounter);
 
 /**
+ * Sets the current MAC frame counter value only if the new value is larger than the current one.
+ *
+ * @param[in]   aInstance         A pointer to an OpenThread instance.
+ * @param[in]   aMacFrameCounter  The MAC frame counter value.
+ *
+ * @retval OT_ERROR_NONE             If successful.
+ * @retval OT_ERROR_INVALID_STATE    If the raw link-layer isn't enabled.
+ *
+ */
+otError otLinkRawSetMacFrameCounterIfLarger(otInstance *aInstance, uint32_t aMacFrameCounter);
+
+/**
  * Get current platform time (64bits width) of the radio chip.
  *
  * @param[in]  aInstance    A pointer to an OpenThread instance.
diff --git a/include/openthread/logging.h b/include/openthread/logging.h
index b07e643..b581b27 100644
--- a/include/openthread/logging.h
+++ b/include/openthread/logging.h
@@ -86,7 +86,7 @@
  * @param[in]  ...      Arguments for the format specification.
  *
  */
-void otLogCritPlat(const char *aFormat, ...);
+void otLogCritPlat(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2);
 
 /**
  * This function emits a log message at warning log level.
@@ -98,7 +98,7 @@
  * @param[in]  ...      Arguments for the format specification.
  *
  */
-void otLogWarnPlat(const char *aFormat, ...);
+void otLogWarnPlat(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2);
 
 /**
  * This function emits a log message at note log level.
@@ -110,7 +110,7 @@
  * @param[in]  ...      Arguments for the format specification.
  *
  */
-void otLogNotePlat(const char *aFormat, ...);
+void otLogNotePlat(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2);
 
 /**
  * This function emits a log message at info log level.
@@ -122,7 +122,7 @@
  * @param[in]  ...      Arguments for the format specification.
  *
  */
-void otLogInfoPlat(const char *aFormat, ...);
+void otLogInfoPlat(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2);
 
 /**
  * This function emits a log message at debug log level.
@@ -134,7 +134,7 @@
  * @param[in]  ...      Arguments for the format specification.
  *
  */
-void otLogDebgPlat(const char *aFormat, ...);
+void otLogDebgPlat(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(1, 2);
 
 /**
  * This function generates a memory dump at critical log level.
@@ -212,7 +212,7 @@
  * @param[in]  ...       Arguments for the format specification.
  *
  */
-void otLogCli(otLogLevel aLogLevel, const char *aFormat, ...);
+void otLogCli(otLogLevel aLogLevel, const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
 
 /**
  * @}
diff --git a/include/openthread/mesh_diag.h b/include/openthread/mesh_diag.h
new file mode 100644
index 0000000..dd613d1
--- /dev/null
+++ b/include/openthread/mesh_diag.h
@@ -0,0 +1,233 @@
+/*
+ *  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 defines the OpenThread Mesh Diagnostic APIs.
+ */
+
+#ifndef OPENTHREAD_MESH_DIAG_H_
+#define OPENTHREAD_MESH_DIAG_H_
+
+#include <openthread/instance.h>
+#include <openthread/thread.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @addtogroup api-mesh-diag
+ *
+ * @brief
+ *   This module includes definitions and functions for Mesh Diagnostics.
+ *
+ *   The Mesh Diagnostics APIs require `OPENTHREAD_CONFIG_MESH_DIAG_ENABLE` and `OPENTHREAD_FTD`.
+ *
+ * @{
+ *
+ */
+
+/**
+ * This structure represents the set of configurations used when discovering mesh topology indicating which items to
+ * discover.
+ *
+ */
+typedef struct otMeshDiagDiscoverConfig
+{
+    bool mDiscoverIp6Addresses : 1; ///< Whether or not to discover IPv6 addresses of every router.
+    bool mDiscoverChildTable : 1;   ///< Whether or not to discover children of every router.
+} otMeshDiagDiscoverConfig;
+
+/**
+ * This type is an opaque iterator to iterate over list of IPv6 addresses of a router.
+ *
+ * Pointers to instance of this type are provided in `otMeshDiagRouterInfo`.
+ *
+ */
+typedef struct otMeshDiagIp6AddrIterator otMeshDiagIp6AddrIterator;
+
+/**
+ * This type is an opaque iterator to iterate over list of children of a router.
+ *
+ * Pointers to instance of this type are provided in `otMeshDiagRouterInfo`.
+ *
+ */
+typedef struct otMeshDiagChildIterator otMeshDiagChildIterator;
+
+/**
+ * This constant indicates that Thread Version is unknown.
+ *
+ * This is used in `otMeshDiagRouterInfo` for `mVersion` property when device does not provide its version. This
+ * indicates that device is likely running 1.3.0 (version value 4) or earlier.
+ *
+ */
+#define OT_MESH_DIAG_VERSION_UNKNOWN 0xffff
+
+/**
+ * This type represents information about a router in Thread mesh.
+ *
+ */
+typedef struct otMeshDiagRouterInfo
+{
+    otExtAddress mExtAddress;             ///< Extended MAC address.
+    uint16_t     mRloc16;                 ///< RLOC16.
+    uint8_t      mRouterId;               ///< Router ID.
+    uint16_t     mVersion;                ///< Thread Version. `OT_MESH_DIAG_VERSION_UNKNOWN` if unknown.
+    bool         mIsThisDevice : 1;       ///< Whether router is this device itself.
+    bool         mIsThisDeviceParent : 1; ///< Whether router is parent of this device (when device is a child).
+    bool         mIsLeader : 1;           ///< Whether router is leader.
+    bool         mIsBorderRouter : 1;     ///< Whether router acts as a border router providing ext connectivity.
+
+    /**
+     * This array provides the link quality from this router to other routers, also indicating whether a link is
+     * established between the routers.
+     *
+     * The array is indexed based on Router ID. `mLinkQualities[routerId]` indicates the incoming link quality, the
+     * router sees to the router with `routerId`. Link quality is a value in [0, 3]. Value zero indicates no link.
+     * Larger value indicate better link quality (as defined by Thread specification).
+     *
+     */
+    uint8_t mLinkQualities[OT_NETWORK_MAX_ROUTER_ID + 1];
+
+    /**
+     * A pointer to an iterator to go through the list of IPv6 addresses of the router.
+     *
+     * The pointer is valid only while `otMeshDiagRouterInfo` is valid. It can be used in `otMeshDiagGetNextIp6Address`
+     * to iterate through the IPv6 addresses.
+     *
+     * The pointer can be NULL when there was no request to discover IPv6 addresses (in `otMeshDiagDiscoverConfig`) or
+     * if the router did not provide the list.
+     *
+     */
+    otMeshDiagIp6AddrIterator *mIp6AddrIterator;
+
+    /**
+     * A pointer to an iterator to go through the list of children of the router.
+     *
+     * The pointer is valid only while `otMeshDiagRouterInfo` is valid. It can be used in `otMeshDiagGetNextChildInfo`
+     * to iterate through the children of the router.
+     *
+     * The pointer can be NULL when there was no request to discover children (in `otMeshDiagDiscoverConfig`) or
+     * if the router did not provide the list.
+     *
+     */
+    otMeshDiagChildIterator *mChildIterator;
+} otMeshDiagRouterInfo;
+
+/**
+ * This type represents information about a discovered child in Thread mesh.
+ *
+ */
+typedef struct otMeshDiagChildInfo
+{
+    uint16_t         mRloc16;             ///< RLOC16.
+    otLinkModeConfig mMode;               ///< Device mode.
+    uint8_t          mLinkQuality;        ///< Incoming link quality to child from parent.
+    bool             mIsThisDevice : 1;   ///< Whether child is this device itself.
+    bool             mIsBorderRouter : 1; ///< Whether child acts as a border router providing ext connectivity.
+} otMeshDiagChildInfo;
+
+/**
+ * This function pointer type represents the callback used by `otMeshDiagDiscoverTopology()` to provide information
+ * about a discovered router.
+ *
+ * When @p aError is `OT_ERROR_PENDING`, it indicates that the discovery is not yet finished and there will be more
+ * routers to discover and the callback will be invoked again.
+ *
+ * @param[in] aError       OT_ERROR_PENDING            Indicates there are more routers to be discovered.
+ *                         OT_ERROR_NONE               Indicates this is the last router and mesh discovery is done.
+ *                         OT_ERROR_RESPONSE_TIMEOUT   Timed out waiting for response from one or more routers.
+ * @param[in] aRouterInfo  The discovered router info (can be null if `aError` is OT_ERROR_RESPONSE_TIMEOUT).
+ * @param[in] aContext     Application-specific context.
+ *
+ */
+typedef void (*otMeshDiagDiscoverCallback)(otError aError, otMeshDiagRouterInfo *aRouterInfo, void *aContext);
+
+/**
+ * This function starts network topology discovery.
+ *
+ * @param[in] aInstance        The OpenThread instance.
+ * @param[in] aConfig          The configuration to use for discovery (e.g., which items to discover).
+ * @param[in] aCallback        The callback to report the discovered routers.
+ * @param[in] aContext         A context to pass in @p aCallback.
+ *
+ * @retval OT_ERROR_NONE            The network topology discovery started successfully.
+ * @retval OT_ERROR_BUSY            A previous discovery request is still ongoing.
+ * @retval OT_ERROR_INVALID_STATE   Device is not attached.
+ * @retval OT_ERROR_NO_BUFS         Could not allocate buffer to send discovery messages.
+ *
+ */
+otError otMeshDiagDiscoverTopology(otInstance                     *aInstance,
+                                   const otMeshDiagDiscoverConfig *aConfig,
+                                   otMeshDiagDiscoverCallback      aCallback,
+                                   void                           *aContext);
+
+/**
+ * This function cancels an ongoing topology discovery if there is one, otherwise no action.
+ *
+ * When ongoing discovery is cancelled, the callback from `otMeshDiagDiscoverTopology()` will not be called anymore.
+ *
+ */
+void otMeshDiagCancel(otInstance *aInstance);
+
+/**
+ * This function iterates through the discovered IPv6 address of a router.
+ *
+ * @param[in,out]  aIterator    The address iterator to use.
+ * @param[out]     aIp6Address  A pointer to return the next IPv6 address (if any).
+ *
+ * @retval OT_ERROR_NONE       Successfully retrieved the next address. @p aIp6Address and @p aIterator are updated.
+ * @retval OT_ERROR_NOT_FOUND  No more address. Reached the end of the list.
+ *
+ */
+otError otMeshDiagGetNextIp6Address(otMeshDiagIp6AddrIterator *aIterator, otIp6Address *aIp6Address);
+
+/**
+ * This function iterates through the discovered children of a router.
+ *
+ * @param[in,out]  aIterator    The address iterator to use.
+ * @param[out]     aChildInfo   A pointer to return the child info (if any).
+ *
+ * @retval OT_ERROR_NONE       Successfully retrieved the next child. @p aChildInfo and @p aIterator are updated.
+ * @retval OT_ERROR_NOT_FOUND  No more child. Reached the end of the list.
+ *
+ */
+otError otMeshDiagGetNextChildInfo(otMeshDiagChildIterator *aIterator, otMeshDiagChildInfo *aChildInfo);
+
+/**
+ * @}
+ *
+ */
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // OPENTHREAD_MESH_DIAG_H_
diff --git a/include/openthread/message.h b/include/openthread/message.h
index d94c176..0826ddb 100644
--- a/include/openthread/message.h
+++ b/include/openthread/message.h
@@ -287,8 +287,16 @@
  */
 typedef struct otBufferInfo
 {
-    uint16_t           mTotalBuffers;         ///< The total number of buffers in the messages pool (0xffff if unknown).
-    uint16_t           mFreeBuffers;          ///< The number of free buffers (0xffff if unknown).
+    uint16_t mTotalBuffers; ///< The total number of buffers in the messages pool (0xffff if unknown).
+    uint16_t mFreeBuffers;  ///< The number of free buffers (0xffff if unknown).
+
+    /**
+     * The maximum number of used buffers at the same time since OT stack initialization or last call to
+     * `otMessageResetBufferInfo()`.
+     *
+     */
+    uint16_t mMaxUsedBuffers;
+
     otMessageQueueInfo m6loSendQueue;         ///< Info about 6LoWPAN send queue.
     otMessageQueueInfo m6loReassemblyQueue;   ///< Info about 6LoWPAN reassembly queue.
     otMessageQueueInfo mIp6Queue;             ///< Info about IPv6 send queue.
@@ -370,6 +378,16 @@
 void otMessageGetBufferInfo(otInstance *aInstance, otBufferInfo *aBufferInfo);
 
 /**
+ * Reset the Message Buffer information counter tracking the maximum number buffers in use at the same time.
+ *
+ * This resets `mMaxUsedBuffers` in `otBufferInfo`.
+ *
+ * @param[in]   aInstance    A pointer to the OpenThread instance.
+ *
+ */
+void otMessageResetBufferInfo(otInstance *aInstance);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/multi_radio.h b/include/openthread/multi_radio.h
index 744b1c0..13930f5 100644
--- a/include/openthread/multi_radio.h
+++ b/include/openthread/multi_radio.h
@@ -85,8 +85,8 @@
  * @retval OT_ERROR_NOT_FOUND   Could not find a neighbor with @p aExtAddress.
  *
  */
-otError otMultiRadioGetNeighborInfo(otInstance *              aInstance,
-                                    const otExtAddress *      aExtAddress,
+otError otMultiRadioGetNeighborInfo(otInstance               *aInstance,
+                                    const otExtAddress       *aExtAddress,
                                     otMultiRadioNeighborInfo *aNeighborInfo);
 
 /**
diff --git a/include/openthread/nat64.h b/include/openthread/nat64.h
index 55bef4d..cd1eec2 100644
--- a/include/openthread/nat64.h
+++ b/include/openthread/nat64.h
@@ -35,6 +35,7 @@
 #ifndef OPENTHREAD_NAT64_H_
 #define OPENTHREAD_NAT64_H_
 
+#include <openthread/ip6.h>
 #include <openthread/message.h>
 
 #ifdef __cplusplus
@@ -45,7 +46,7 @@
  * @addtogroup api-nat64
  *
  * @brief This module includes functions and structs for the NAT64 function on the border router. These functions are
- * only available when `OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE` is enabled.
+ * only available when `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` is enabled.
  *
  * @{
  *
@@ -88,6 +89,407 @@
 } otIp4Cidr;
 
 /**
+ * Represents the counters for NAT64.
+ *
+ */
+typedef struct otNat64Counters
+{
+    uint64_t m4To6Packets; ///< Number of packets translated from IPv4 to IPv6.
+    uint64_t m4To6Bytes;   ///< Sum of size of packets translated from IPv4 to IPv6.
+    uint64_t m6To4Packets; ///< Number of packets translated from IPv6 to IPv4.
+    uint64_t m6To4Bytes;   ///< Sum of size of packets translated from IPv6 to IPv4.
+} otNat64Counters;
+
+/**
+ * Represents the counters for the protocols supported by NAT64.
+ *
+ */
+typedef struct otNat64ProtocolCounters
+{
+    otNat64Counters mTotal; ///< Counters for sum of all protocols.
+    otNat64Counters mIcmp;  ///< Counters for ICMP and ICMPv6.
+    otNat64Counters mUdp;   ///< Counters for UDP.
+    otNat64Counters mTcp;   ///< Counters for TCP.
+} otNat64ProtocolCounters;
+
+/**
+ * Packet drop reasons.
+ *
+ */
+typedef enum otNat64DropReason
+{
+    OT_NAT64_DROP_REASON_UNKNOWN = 0,       ///< Packet drop for unknown reasons.
+    OT_NAT64_DROP_REASON_ILLEGAL_PACKET,    ///< Packet drop due to failed to parse the datagram.
+    OT_NAT64_DROP_REASON_UNSUPPORTED_PROTO, ///< Packet drop due to unsupported IP protocol.
+    OT_NAT64_DROP_REASON_NO_MAPPING,        ///< Packet drop due to no mappings found or mapping pool exhausted.
+    //---
+    OT_NAT64_DROP_REASON_COUNT,
+} otNat64DropReason;
+
+/**
+ * Represents the counters of dropped packets due to errors when handling NAT64 packets.
+ *
+ */
+typedef struct otNat64ErrorCounters
+{
+    uint64_t mCount4To6[OT_NAT64_DROP_REASON_COUNT]; ///< Errors translating IPv4 packets.
+    uint64_t mCount6To4[OT_NAT64_DROP_REASON_COUNT]; ///< Errors translating IPv6 packets.
+} otNat64ErrorCounters;
+
+/**
+ * Gets NAT64 translator counters.
+ *
+ * The counter is counted since the instance initialized.
+ *
+ * Available when `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled.
+ *
+ * @param[in]  aInstance A pointer to an OpenThread instance.
+ * @param[out] aCounters A pointer to an `otNat64Counters` where the counters of NAT64 translator will be placed.
+ *
+ */
+void otNat64GetCounters(otInstance *aInstance, otNat64ProtocolCounters *aCounters);
+
+/**
+ * Gets the NAT64 translator error counters.
+ *
+ * The counters are initialized to zero when the OpenThread instance is initialized.
+ *
+ * @param[in]  aInstance A pointer to an OpenThread instance.
+ * @param[out] aCounters A pointer to an `otNat64Counters` where the counters of NAT64 translator will be placed.
+ *
+ */
+void otNat64GetErrorCounters(otInstance *aInstance, otNat64ErrorCounters *aCounters);
+
+/**
+ * Represents an address mapping record for NAT64.
+ *
+ * @note The counters will be reset for each mapping session even for the same address pair. Applications can use `mId`
+ * to identify different sessions to calculate the packets correctly.
+ *
+ */
+typedef struct otNat64AddressMapping
+{
+    uint64_t mId; ///< The unique id for a mapping session.
+
+    otIp4Address mIp4;             ///< The IPv4 address of the mapping.
+    otIp6Address mIp6;             ///< The IPv6 address of the mapping.
+    uint32_t     mRemainingTimeMs; ///< Remaining time before expiry in milliseconds.
+
+    otNat64ProtocolCounters mCounters;
+} otNat64AddressMapping;
+
+/**
+ * Used to iterate through NAT64 address mappings.
+ *
+ * The fields in this type are opaque (intended for use by OpenThread core only) and therefore should not be
+ * accessed or used by caller.
+ *
+ * Before using an iterator, it MUST be initialized using `otNat64AddressMappingIteratorInit()`.
+ *
+ */
+typedef struct otNat64AddressMappingIterator
+{
+    void *mPtr;
+} otNat64AddressMappingIterator;
+
+/**
+ * Initializes an `otNat64AddressMappingIterator`.
+ *
+ * An iterator MUST be initialized before it is used.
+ *
+ * An iterator can be initialized again to restart from the beginning of the mapping info.
+ *
+ * @param[in]  aInstance  The OpenThread instance.
+ * @param[out] aIterator  A pointer to the iterator to initialize.
+ *
+ */
+void otNat64InitAddressMappingIterator(otInstance *aInstance, otNat64AddressMappingIterator *aIterator);
+
+/**
+ * Gets the next AddressMapping info (using an iterator).
+ *
+ * Available when `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled.
+ *
+ * @param[in]      aInstance      A pointer to an OpenThread instance.
+ * @param[in,out]  aIterator      A pointer to the iterator. On success the iterator will be updated to point to next
+ *                                NAT64 address mapping record. To get the first entry the iterator should be set to
+ *                                OT_NAT64_ADDRESS_MAPPING_ITERATOR_INIT.
+ * @param[out]     aMapping       A pointer to an `otNat64AddressMapping` where information of next NAT64 address
+ *                                mapping record is placed (on success).
+ *
+ * @retval OT_ERROR_NONE       Successfully found the next NAT64 address mapping info (@p aMapping was successfully
+ *                             updated).
+ * @retval OT_ERROR_NOT_FOUND  No subsequent NAT64 address mapping info was found.
+ *
+ */
+otError otNat64GetNextAddressMapping(otInstance                    *aInstance,
+                                     otNat64AddressMappingIterator *aIterator,
+                                     otNat64AddressMapping         *aMapping);
+
+/**
+ * States of NAT64.
+ *
+ */
+typedef enum
+{
+    OT_NAT64_STATE_DISABLED = 0, ///< NAT64 is disabled.
+    OT_NAT64_STATE_NOT_RUNNING,  ///< NAT64 is enabled, but one or more dependencies of NAT64 are not running.
+    OT_NAT64_STATE_IDLE,         ///< NAT64 is enabled, but this BR is not an active NAT64 BR.
+    OT_NAT64_STATE_ACTIVE,       ///< The BR is publishing a NAT64 prefix and/or translating packets.
+} otNat64State;
+
+/**
+ * Gets the state of NAT64 translator.
+ *
+ * Available when `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled.
+ *
+ * @param[in]  aInstance          A pointer to an OpenThread instance.
+ *
+ * @retval OT_NAT64_STATE_DISABLED    NAT64 translator is disabled.
+ * @retval OT_NAT64_STATE_NOT_RUNNING NAT64 translator is enabled, but the translator is not configured with a valid
+ *                                    NAT64 prefix and a CIDR.
+ * @retval OT_NAT64_STATE_ACTIVE      NAT64 translator is enabled, and is translating packets.
+ *
+ */
+otNat64State otNat64GetTranslatorState(otInstance *aInstance);
+
+/**
+ * Gets the state of NAT64 prefix manager.
+ *
+ * Available when `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` is enabled.
+ *
+ * @param[in]  aInstance          A pointer to an OpenThread instance.
+ *
+ * @retval OT_NAT64_STATE_DISABLED    NAT64 prefix manager is disabled.
+ * @retval OT_NAT64_STATE_NOT_RUNNING NAT64 prefix manager is enabled, but is not running (because the routing manager
+ *                                    is not running).
+ * @retval OT_NAT64_STATE_IDLE        NAT64 prefix manager is enabled, but is not publishing a NAT64 prefix. Usually
+ *                                    when there is another border router publishing a NAT64 prefix with higher
+ *                                    priority.
+ * @retval OT_NAT64_STATE_ACTIVE      NAT64 prefix manager is enabled, and is publishing NAT64 prefix to the Thread
+ *                                    network.
+ *
+ */
+otNat64State otNat64GetPrefixManagerState(otInstance *aInstance);
+
+/**
+ * Enable or disable NAT64 functions.
+ *
+ * Note: This includes the NAT64 Translator (when `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled) and the NAT64
+ * Prefix Manager (when `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` is enabled).
+ *
+ * When `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled, setting disabled to true resets the
+ * mapping table in the translator.
+ *
+ * Available when `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` or `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` is
+ * enabled.
+ *
+ * @param[in]  aInstance  A pointer to an OpenThread instance.
+ * @param[in]  aEnabled   A boolean to enable/disable the NAT64 functions
+ *
+ * @sa otNat64GetTranslatorState
+ * @sa otNat64GetPrefixManagerState
+ *
+ */
+void otNat64SetEnabled(otInstance *aInstance, bool aEnable);
+
+/**
+ * Allocate a new message buffer for sending an IPv4 message to the NAT64 translator.
+ *
+ * Message buffers allocated by this function will have 20 bytes (difference between the size of IPv6 headers
+ * and IPv4 header sizes) reserved.
+ *
+ * Available when `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled.
+ *
+ * @note If @p aSettings is `NULL`, the link layer security is enabled and the message priority is set to
+ * OT_MESSAGE_PRIORITY_NORMAL by default.
+ *
+ * @param[in]  aInstance  A pointer to an OpenThread instance.
+ * @param[in]  aSettings  A pointer to the message settings or NULL to set default settings.
+ *
+ * @returns A pointer to the message buffer or NULL if no message buffers are available or parameters are invalid.
+ *
+ * @sa otNat64Send
+ *
+ */
+otMessage *otIp4NewMessage(otInstance *aInstance, const otMessageSettings *aSettings);
+
+/**
+ * Sets the CIDR used when setting the source address of the outgoing translated IPv4 packets.
+ *
+ * This function is available only when OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE is enabled.
+ *
+ * @note A valid CIDR must have a non-zero prefix length. The actual addresses pool is limited by the size of the
+ * mapping pool and the number of addresses available in the CIDR block.
+ *
+ * @note This function can be called at any time, but the NAT64 translator will be reset and all existing sessions will
+ * be expired when updating the configured CIDR.
+ *
+ * @param[in] aInstance  A pointer to an OpenThread instance.
+ * @param[in] aCidr      A pointer to an otIp4Cidr for the IPv4 CIDR block for NAT64.
+ *
+ * @retval  OT_ERROR_INVALID_ARGS   The given CIDR is not a valid IPv4 CIDR for NAT64.
+ * @retval  OT_ERROR_NONE           Successfully set the CIDR for NAT64.
+ *
+ * @sa otBorderRouterSend
+ * @sa otBorderRouterSetReceiveCallback
+ *
+ */
+otError otNat64SetIp4Cidr(otInstance *aInstance, const otIp4Cidr *aCidr);
+
+/**
+ * Translates an IPv4 datagram to an IPv6 datagram and sends via the Thread interface.
+ *
+ * The caller transfers ownership of @p aMessage when making this call. OpenThread will free @p aMessage when
+ * processing is complete, including when a value other than `OT_ERROR_NONE` is returned.
+ *
+ * @param[in]  aInstance A pointer to an OpenThread instance.
+ * @param[in]  aMessage  A pointer to the message buffer containing the IPv4 datagram.
+ *
+ * @retval OT_ERROR_NONE                    Successfully processed the message.
+ * @retval OT_ERROR_DROP                    Message was well-formed but not fully processed due to packet processing
+ *                                          rules.
+ * @retval OT_ERROR_NO_BUFS                 Could not allocate necessary message buffers when processing the datagram.
+ * @retval OT_ERROR_NO_ROUTE                No route to host.
+ * @retval OT_ERROR_INVALID_SOURCE_ADDRESS  Source address is invalid, e.g. an anycast address or a multicast address.
+ * @retval OT_ERROR_PARSE                   Encountered a malformed header when processing the message.
+ *
+ */
+otError otNat64Send(otInstance *aInstance, otMessage *aMessage);
+
+/**
+ * This function pointer is called when an IPv4 datagram (translated by NAT64 translator) is received.
+ *
+ * @param[in]  aMessage  A pointer to the message buffer containing the received IPv6 datagram. This function transfers
+ *                       the ownership of the @p aMessage to the receiver of the callback. The message should be
+ *                       freed by the receiver of the callback after it is processed.
+ * @param[in]  aContext  A pointer to application-specific context.
+ *
+ */
+typedef void (*otNat64ReceiveIp4Callback)(otMessage *aMessage, void *aContext);
+
+/**
+ * Registers a callback to provide received IPv4 datagrams.
+ *
+ * @param[in]  aInstance         A pointer to an OpenThread instance.
+ * @param[in]  aCallback         A pointer to a function that is called when an IPv4 datagram is received or
+ *                               NULL to disable the callback.
+ * @param[in]  aCallbackContext  A pointer to application-specific context.
+ *
+ */
+void otNat64SetReceiveIp4Callback(otInstance *aInstance, otNat64ReceiveIp4Callback aCallback, void *aContext);
+
+/**
+ * Gets the IPv4 CIDR configured in the NAT64 translator.
+ *
+ * Available when `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled.
+ *
+ * @param[in]  aInstance         A pointer to an OpenThread instance.
+ * @param[out] aCidr             A pointer to an otIp4Cidr. Where the CIDR will be filled.
+ *
+ */
+otError otNat64GetCidr(otInstance *aInstance, otIp4Cidr *aCidr);
+
+/**
+ * Test if two IPv4 addresses are the same.
+ *
+ * @param[in]  aFirst   A pointer to the first IPv4 address to compare.
+ * @param[in]  aSecond  A pointer to the second IPv4 address to compare.
+ *
+ * @retval TRUE   The two IPv4 addresses are the same.
+ * @retval FALSE  The two IPv4 addresses are not the same.
+ *
+ */
+bool otIp4IsAddressEqual(const otIp4Address *aFirst, const otIp4Address *aSecond);
+
+/**
+ * Set @p aIp4Address by performing NAT64 address translation from @p aIp6Address as specified
+ * in RFC 6052.
+ *
+ * The NAT64 @p aPrefixLength MUST be one of the following values: 32, 40, 48, 56, 64, or 96, otherwise the behavior
+ * of this method is undefined.
+ *
+ * @param[in]  aPrefixLength  The prefix length to use for IPv4/IPv6 translation.
+ * @param[in]  aIp6Address    A pointer to an IPv6 address.
+ * @param[out] aIp4Address    A pointer to output the IPv4 address.
+ *
+ */
+void otIp4ExtractFromIp6Address(uint8_t aPrefixLength, const otIp6Address *aIp6Address, otIp4Address *aIp4Address);
+
+#define OT_IP4_ADDRESS_STRING_SIZE 17 ///< Length of 000.000.000.000 plus a suffix NUL
+
+/**
+ * Converts the address to a string.
+ *
+ * The string format uses quad-dotted notation of four bytes in the address (e.g., "127.0.0.1").
+ *
+ * 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.
+ *
+ * @param[in]  aAddress  A pointer to an IPv4 address (MUST NOT be NULL).
+ * @param[out] aBuffer   A pointer to a char array to output the string (MUST NOT be `nullptr`).
+ * @param[in]  aSize     The size of @p aBuffer (in bytes).
+ *
+ */
+void otIp4AddressToString(const otIp4Address *aAddress, char *aBuffer, uint16_t aSize);
+
+#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.,
+ * "127.0.0.1/32").
+ *
+ * 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.
+ *
+ * @param[in]  aCidr     A pointer to an IPv4 CIDR (MUST NOT be NULL).
+ * @param[out] aBuffer   A pointer to a char array to output the string (MUST NOT be `nullptr`).
+ * @param[in]  aSize     The size of @p aBuffer (in bytes).
+ *
+ */
+void otIp4CidrToString(const otIp4Cidr *aCidr, char *aBuffer, uint16_t aSize);
+
+/**
+ * Converts a human-readable IPv4 address string into a binary representation.
+ *
+ * @param[in]   aString   A pointer to a NULL-terminated string.
+ * @param[out]  aAddress  A pointer to an IPv4 address.
+ *
+ * @retval OT_ERROR_NONE          Successfully parsed the string.
+ * @retval OT_ERROR_INVALID_ARGS  Failed to parse the string.
+ *
+ */
+otError otIp4AddressFromString(const char *aString, otIp4Address *aAddress);
+
+/**
+ * Sets the IPv6 address by performing NAT64 address translation from the preferred NAT64 prefix and the given IPv4
+ * address as specified in RFC 6052.
+ *
+ * @param[in]   aInstance    A pointer to an OpenThread instance.
+ * @param[in]   aIp4Address  A pointer to the IPv4 address to translate to IPv6.
+ * @param[out]  aIp6Address  A pointer to the synthesized IPv6 address.
+ *
+ * @returns  OT_ERROR_NONE           Successfully synthesized the IPv6 address from NAT64 prefix and IPv4 address.
+ * @returns  OT_ERROR_INVALID_STATE  No valid NAT64 prefix in the network data.
+ *
+ */
+otError otNat64SynthesizeIp6Address(otInstance *aInstance, const otIp4Address *aIp4Address, otIp6Address *aIp6Address);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/ncp.h b/include/openthread/ncp.h
index 7289672..4576c7f 100644
--- a/include/openthread/ncp.h
+++ b/include/openthread/ncp.h
@@ -158,94 +158,9 @@
  * @param[in] aAllowPokeDelegate      Delegate function pointer for poke operation.
  *
  */
-void otNcpRegisterPeekPokeDelagates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
+void otNcpRegisterPeekPokeDelegates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
                                     otNcpDelegateAllowPeekPoke aAllowPokeDelegate);
 
-//-----------------------------------------------------------------------------------------
-// Legacy network APIs
-
-#define OT_NCP_LEGACY_ULA_PREFIX_LENGTH 8 ///< Legacy ULA size (in bytes)
-
-/**
- * Defines handler (function pointer) type for starting legacy network
- *
- * Invoked to start the legacy network.
- *
- */
-typedef void (*otNcpHandlerStartLegacy)(void);
-
-/**
- * Defines handler (function pointer) type for stopping legacy network
- *
- * Invoked to stop the legacy network.
- *
- */
-typedef void (*otNcpHandlerStopLegacy)(void);
-
-/**
- * Defines handler (function pointer) type for initiating joining process.
- *
- * @param[in] aExtAddress   A pointer to the extended address for the node to join
- *                          or NULL if desired to join any neighboring node.
- *
- * Invoked to initiate a legacy join procedure to any or a specific node.
- *
- */
-typedef void (*otNcpHandlerJoinLegacyNode)(const otExtAddress *aExtAddress);
-
-/**
- * Defines handler (function pointer) type for setting the legacy ULA prefix.
- *
- * @param[in] aUlaPrefix   A pointer to buffer containing the legacy ULA prefix.
- *
- * Invoked to set the legacy ULA prefix.
- *
- */
-typedef void (*otNcpHandlerSetLegacyUlaPrefix)(const uint8_t *aUlaPrefix);
-
-/**
- * Defines a struct containing all the legacy handlers (function pointers).
- *
- */
-typedef struct otNcpLegacyHandlers
-{
-    otNcpHandlerStartLegacy        mStartLegacy;        ///< Start handler
-    otNcpHandlerStopLegacy         mStopLegacy;         ///< Stop handler
-    otNcpHandlerJoinLegacyNode     mJoinLegacyNode;     ///< Join handler
-    otNcpHandlerSetLegacyUlaPrefix mSetLegacyUlaPrefix; ///< Set ULA handler
-} otNcpLegacyHandlers;
-
-/**
- * This callback is invoked by the legacy stack to notify that a new
- * legacy node did join the network.
- *
- * @param[in]   aExtAddr    A pointer to the extended address of the joined node.
- *
- */
-void otNcpHandleLegacyNodeDidJoin(const otExtAddress *aExtAddr);
-
-/**
- * This callback is invoked by the legacy stack to notify that the
- * legacy ULA prefix has changed.
- *
- * @param[in]    aUlaPrefix  A pointer to the received ULA prefix.
- *
- */
-void otNcpHandleDidReceiveNewLegacyUlaPrefix(const uint8_t *aUlaPrefix);
-
-/**
- * This method registers a set of legacy handlers with NCP.
- *
- * The set of handlers provided by the struct @p aHandlers are used by
- * NCP code to start/stop legacy network.
- * The @p aHandlers can be NULL to disable legacy support on NCP.
- * Individual handlers in the given handlers struct can also be NULL.
- *
- * @param[in] aHandlers    A pointer to a handler struct.
- *
- */
-void otNcpRegisterLegacyHandlers(const otNcpLegacyHandlers *aHandlers);
-
 /**
  * @}
  *
diff --git a/include/openthread/netdata.h b/include/openthread/netdata.h
index 74b92e8..ba99921 100644
--- a/include/openthread/netdata.h
+++ b/include/openthread/netdata.h
@@ -72,6 +72,17 @@
 } otBorderRouterConfig;
 
 /**
+ * This structure represents 6LoWPAN Context ID information associated with a prefix in Network Data.
+ *
+ */
+typedef struct otLowpanContextInfo
+{
+    uint8_t     mContextId;    ///< The 6LoWPAN Context ID.
+    bool        mCompressFlag; ///< The compress flag.
+    otIp6Prefix mPrefix;       ///< The associated IPv6 prefix.
+} otLowpanContextInfo;
+
+/**
  * This structure represents an External Route configuration.
  *
  */
@@ -125,7 +136,7 @@
 } otServiceConfig;
 
 /**
- * This method provides a full or stable copy of the Partition's Thread Network Data.
+ * Provide full or stable copy of the Partition's Thread Network Data.
  *
  * @param[in]      aInstance    A pointer to an OpenThread instance.
  * @param[in]      aStable      TRUE when copying the stable version, FALSE when copying the full version.
@@ -133,11 +144,45 @@
  * @param[in,out]  aDataLength  On entry, size of the data buffer pointed to by @p aData.
  *                              On exit, number of copied bytes.
  *
+ * @retval OT_ERROR_NONE    Successfully copied the Thread Network Data into @p aData and updated @p aDataLength.
+ * @retval OT_ERROR_NO_BUFS Not enough space in @p aData to fully copy the Thread Network Data.
+ *
  */
 otError otNetDataGet(otInstance *aInstance, bool aStable, uint8_t *aData, uint8_t *aDataLength);
 
 /**
- * This function gets the next On Mesh Prefix in the partition's Network Data.
+ * Get the current length (number of bytes) of Partition's Thread Network Data.
+ *
+ * @param[in] aInstance    A pointer to an OpenThread instance.
+ *
+ * @return The length of the Network Data.
+ *
+ */
+uint8_t otNetDataGetLength(otInstance *aInstance);
+
+/**
+ * Get the maximum observed length of the Thread Network Data since OT stack initialization or since the last call to
+ * `otNetDataResetMaxLength()`.
+ *
+ * @param[in] aInstance    A pointer to an OpenThread instance.
+ *
+ * @return The maximum length of the Network Data (high water mark for Network Data length).
+ *
+ */
+uint8_t otNetDataGetMaxLength(otInstance *aInstance);
+
+/**
+ * Reset the tracked maximum length of the Thread Network Data.
+ *
+ * @param[in] aInstance    A pointer to an OpenThread instance.
+ *
+ * @sa otNetDataGetMaxLength
+ *
+ */
+void otNetDataResetMaxLength(otInstance *aInstance);
+
+/**
+ * Get the next On Mesh Prefix in the partition's Network Data.
  *
  * @param[in]      aInstance  A pointer to an OpenThread instance.
  * @param[in,out]  aIterator  A pointer to the Network Data iterator context. To get the first on-mesh entry
@@ -148,12 +193,12 @@
  * @retval OT_ERROR_NOT_FOUND  No subsequent On Mesh prefix exists in the Thread Network Data.
  *
  */
-otError otNetDataGetNextOnMeshPrefix(otInstance *           aInstance,
+otError otNetDataGetNextOnMeshPrefix(otInstance            *aInstance,
                                      otNetworkDataIterator *aIterator,
-                                     otBorderRouterConfig * aConfig);
+                                     otBorderRouterConfig  *aConfig);
 
 /**
- * This function gets the next external route in the partition's Network Data.
+ * Get the next external route in the partition's Network Data.
  *
  * @param[in]      aInstance  A pointer to an OpenThread instance.
  * @param[in,out]  aIterator  A pointer to the Network Data iterator context. To get the first external route entry
@@ -167,7 +212,7 @@
 otError otNetDataGetNextRoute(otInstance *aInstance, otNetworkDataIterator *aIterator, otExternalRouteConfig *aConfig);
 
 /**
- * This function gets the next service in the partition's Network Data.
+ * Get the next service in the partition's Network Data.
  *
  * @param[in]      aInstance  A pointer to an OpenThread instance.
  * @param[in,out]  aIterator  A pointer to the Network Data iterator context. To get the first service entry
@@ -181,6 +226,22 @@
 otError otNetDataGetNextService(otInstance *aInstance, otNetworkDataIterator *aIterator, otServiceConfig *aConfig);
 
 /**
+ * Get the next 6LoWPAN Context ID info in the partition's Network Data.
+ *
+ * @param[in]      aInstance     A pointer to an OpenThread instance.
+ * @param[in,out]  aIterator     A pointer to the Network Data iterator. To get the first service entry
+                                 it should be set to OT_NETWORK_DATA_ITERATOR_INIT.
+ * @param[out]     aContextInfo  A pointer to where the retrieved 6LoWPAN Context ID information will be placed.
+ *
+ * @retval OT_ERROR_NONE       Successfully found the next 6LoWPAN Context ID info.
+ * @retval OT_ERROR_NOT_FOUND  No subsequent 6LoWPAN Context info exists in the partition's Network Data.
+ *
+ */
+otError otNetDataGetNextLowpanContextInfo(otInstance            *aInstance,
+                                          otNetworkDataIterator *aIterator,
+                                          otLowpanContextInfo   *aContextInfo);
+
+/**
  * Get the Network Data Version.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
@@ -227,12 +288,11 @@
  * @retval OT_ERROR_NOT_FOUND     @p aDiscerner is not included in the steering data.
  *
  */
-otError otNetDataSteeringDataCheckJoinerWithDiscerner(otInstance *                    aInstance,
+otError otNetDataSteeringDataCheckJoinerWithDiscerner(otInstance                     *aInstance,
                                                       const struct otJoinerDiscerner *aDiscerner);
 
 /**
- * This function checks whether a given Prefix can act as a valid OMR prefix and also the Leader's Network Data contains
- * this prefix.
+ * Check whether a given Prefix can act as a valid OMR prefix and also the Leader's Network Data contains this prefix.
  *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  * @param[in]  aPrefix    A pointer to the IPv6 prefix.
diff --git a/include/openthread/netdata_publisher.h b/include/openthread/netdata_publisher.h
index 2c38e15..10346c2 100644
--- a/include/openthread/netdata_publisher.h
+++ b/include/openthread/netdata_publisher.h
@@ -91,8 +91,8 @@
  *
  */
 typedef void (*otNetDataPrefixPublisherCallback)(otNetDataPublisherEvent aEvent,
-                                                 const otIp6Prefix *     aPrefix,
-                                                 void *                  aContext);
+                                                 const otIp6Prefix      *aPrefix,
+                                                 void                   *aContext);
 
 /**
  * This function requests "DNS/SRP Service Anycast Address" to be published in the Thread Network Data.
@@ -170,9 +170,9 @@
  * @param[in] aContext         A pointer to application-specific context (used when @p aCallback is invoked).
  *
  */
-void otNetDataSetDnsSrpServicePublisherCallback(otInstance *                            aInstance,
+void otNetDataSetDnsSrpServicePublisherCallback(otInstance                             *aInstance,
                                                 otNetDataDnsSrpServicePublisherCallback aCallback,
-                                                void *                                  aContext);
+                                                void                                   *aContext);
 
 /**
  * Unpublishes any previously added DNS/SRP (Anycast or Unicast) Service entry from the Thread Network
@@ -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.
  *
@@ -265,9 +301,9 @@
  * @param[in] aContext         A pointer to application-specific context (used when @p aCallback is invoked).
  *
  */
-void otNetDataSetPrefixPublisherCallback(otInstance *                     aInstance,
+void otNetDataSetPrefixPublisherCallback(otInstance                      *aInstance,
                                          otNetDataPrefixPublisherCallback aCallback,
-                                         void *                           aContext);
+                                         void                            *aContext);
 
 /**
  * Unpublishes a previously published On-Mesh or External Route Prefix.
diff --git a/include/openthread/netdiag.h b/include/openthread/netdiag.h
index d402383..76e523b 100644
--- a/include/openthread/netdiag.h
+++ b/include/openthread/netdiag.h
@@ -66,24 +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_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.
 
 /**
@@ -203,6 +213,14 @@
     uint16_t mTimeout : 5;
 
     /**
+     * Link Quality In value in [0,3].
+     *
+     * Value 0 indicates that sender does not support the feature to provide link quality info.
+     *
+     */
+    uint8_t mLinkQuality : 2;
+
+    /**
      * Child ID from which an RLOC can be generated.
      */
     uint16_t mChildId : 9;
@@ -237,6 +255,11 @@
         uint8_t                   mBatteryLevel;
         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;
@@ -264,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.
@@ -276,9 +301,9 @@
  * @Note A subsequent call to this function is allowed only when current return value is OT_ERROR_NONE.
  *
  */
-otError otThreadGetNextDiagnosticTlv(const otMessage *      aMessage,
+otError otThreadGetNextDiagnosticTlv(const otMessage       *aMessage,
                                      otNetworkDiagIterator *aIterator,
-                                     otNetworkDiagTlv *     aNetworkDiagTlv);
+                                     otNetworkDiagTlv      *aNetworkDiagTlv);
 
 /**
  * This function pointer is called when Network Diagnostic Get response is received.
@@ -292,13 +317,15 @@
  *
  */
 typedef void (*otReceiveDiagnosticGetCallback)(otError              aError,
-                                               otMessage *          aMessage,
+                                               otMessage           *aMessage,
                                                const otMessageInfo *aMessageInfo,
-                                               void *               aContext);
+                                               void                *aContext);
 
 /**
  * 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.
@@ -311,16 +338,18 @@
  * @retval OT_ERROR_NO_BUFS Insufficient message buffers available to send DIAG_GET.req.
  *
  */
-otError otThreadSendDiagnosticGet(otInstance *                   aInstance,
-                                  const otIp6Address *           aDestination,
+otError otThreadSendDiagnosticGet(otInstance                    *aInstance,
+                                  const otIp6Address            *aDestination,
                                   const uint8_t                  aTlvTypes[],
                                   uint8_t                        aCount,
                                   otReceiveDiagnosticGetCallback aCallback,
-                                  void *                         aCallbackContext);
+                                  void                          *aCallbackContext);
 
 /**
  * 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.
@@ -330,12 +359,93 @@
  * @retval OT_ERROR_NO_BUFS Insufficient message buffers available to send DIAG_RST.ntf.
  *
  */
-otError otThreadSendDiagnosticReset(otInstance *        aInstance,
+otError otThreadSendDiagnosticReset(otInstance         *aInstance,
                                     const otIp6Address *aDestination,
                                     const uint8_t       aTlvTypes[],
                                     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/network_time.h b/include/openthread/network_time.h
index 5e77231..e20a9d5 100644
--- a/include/openthread/network_time.h
+++ b/include/openthread/network_time.h
@@ -145,9 +145,9 @@
  * @param[in] aCallbackContext The context to be passed to the callback function upon invocation
  *
  */
-void otNetworkTimeSyncSetCallback(otInstance *                aInstance,
+void otNetworkTimeSyncSetCallback(otInstance                 *aInstance,
                                   otNetworkTimeSyncCallbackFn aCallbackFn,
-                                  void *                      aCallbackContext);
+                                  void                       *aCallbackContext);
 
 /**
  * @}
diff --git a/include/openthread/ping_sender.h b/include/openthread/ping_sender.h
index 07e5c03..a0e30ca 100644
--- a/include/openthread/ping_sender.h
+++ b/include/openthread/ping_sender.h
@@ -112,7 +112,7 @@
     otPingSenderReplyCallback mReplyCallback; ///< Callback function to report replies (can be NULL if not needed).
     otPingSenderStatisticsCallback
              mStatisticsCallback; ///< Callback function to report statistics (can be NULL if not needed).
-    void *   mCallbackContext;    ///< A pointer to the callback application-specific context.
+    void    *mCallbackContext;    ///< A pointer to the callback application-specific context.
     uint16_t mSize;               ///< Data size (# of bytes) excludes IPv6/ICMPv6 header. Zero for default.
     uint16_t mCount;              ///< Number of ping messages to send. Zero to use default.
     uint32_t mInterval;           ///< Ping tx interval in milliseconds. Zero to use default.
diff --git a/include/openthread/platform/alarm-micro.h b/include/openthread/platform/alarm-micro.h
index e616407..c165a40 100644
--- a/include/openthread/platform/alarm-micro.h
+++ b/include/openthread/platform/alarm-micro.h
@@ -53,6 +53,8 @@
 /**
  * Set the alarm to fire at @p aDt microseconds after @p aT0.
  *
+ * For both @p aT0 and @p aDt, the platform MUST support all values in [0, 2^32-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 +73,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..0ddd918 100644
--- a/include/openthread/platform/alarm-milli.h
+++ b/include/openthread/platform/alarm-milli.h
@@ -56,6 +56,8 @@
 /**
  * Set the alarm to fire at @p aDt milliseconds after @p aT0.
  *
+ * For both @p aT0 and @p aDt, the platform MUST support all values in [0, 2^32-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 +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 milliseconds.
  */
 uint32_t otPlatAlarmMilliGetNow(void);
diff --git a/include/openthread/platform/crypto.h b/include/openthread/platform/crypto.h
index 7cadc8e..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.
 };
 
 /**
@@ -126,11 +129,106 @@
  */
 typedef struct otCryptoContext
 {
-    void *   mContext;     ///< Pointer to the context.
+    void    *mContext;     ///< Pointer to the context.
     uint16_t mContextSize; ///< The length of the context in bytes.
 } otCryptoContext;
 
 /**
+ * Length of SHA256 hash (in bytes).
+ *
+ */
+#define OT_CRYPTO_SHA256_HASH_SIZE 32
+
+/**
+ * @struct otPlatCryptoSha256Hash
+ *
+ * This structure represents a SHA-256 hash.
+ *
+ */
+OT_TOOL_PACKED_BEGIN
+struct otPlatCryptoSha256Hash
+{
+    uint8_t m8[OT_CRYPTO_SHA256_HASH_SIZE]; ///< Hash bytes.
+} OT_TOOL_PACKED_END;
+
+/**
+ * This structure represents a SHA-256 hash.
+ *
+ */
+typedef struct otPlatCryptoSha256Hash otPlatCryptoSha256Hash;
+
+/**
+ * Max buffer size (in bytes) for representing the EDCSA key-pair in DER format.
+ *
+ */
+#define OT_CRYPTO_ECDSA_MAX_DER_SIZE 125
+
+/**
+ * @struct otPlatCryptoEcdsaKeyPair
+ *
+ * This structure represents an ECDSA key pair (public and private keys).
+ *
+ * The key pair is stored using Distinguished Encoding Rules (DER) format (per RFC 5915).
+ *
+ */
+typedef struct otPlatCryptoEcdsaKeyPair
+{
+    uint8_t mDerBytes[OT_CRYPTO_ECDSA_MAX_DER_SIZE];
+    uint8_t mDerLength;
+} otPlatCryptoEcdsaKeyPair;
+
+/**
+ * Buffer size (in bytes) for representing the EDCSA public key.
+ *
+ */
+#define OT_CRYPTO_ECDSA_PUBLIC_KEY_SIZE 64
+
+/**
+ * @struct otPlatCryptoEcdsaPublicKey
+ *
+ * This struct represents a ECDSA public key.
+ *
+ * The public key is stored as a byte sequence representation of an uncompressed curve point (RFC 6605 - sec 4).
+ *
+ */
+OT_TOOL_PACKED_BEGIN
+struct otPlatCryptoEcdsaPublicKey
+{
+    uint8_t m8[OT_CRYPTO_ECDSA_PUBLIC_KEY_SIZE];
+} OT_TOOL_PACKED_END;
+
+typedef struct otPlatCryptoEcdsaPublicKey otPlatCryptoEcdsaPublicKey;
+
+/**
+ * Buffer size (in bytes) for representing the EDCSA signature.
+ *
+ */
+#define OT_CRYPTO_ECDSA_SIGNATURE_SIZE 64
+
+/**
+ * @struct otPlatCryptoEcdsaSignature
+ *
+ * This struct represents an ECDSA signature.
+ *
+ * The signature is encoded as the concatenated binary representation of two MPIs `r` and `s` which are calculated
+ * during signing (RFC 6605 - section 4).
+ *
+ */
+OT_TOOL_PACKED_BEGIN
+struct otPlatCryptoEcdsaSignature
+{
+    uint8_t m8[OT_CRYPTO_ECDSA_SIGNATURE_SIZE];
+} OT_TOOL_PACKED_END;
+
+typedef struct otPlatCryptoEcdsaSignature otPlatCryptoEcdsaSignature;
+
+/**
+ * Max PBKDF2 SALT length: salt prefix (6) + extended panid (8) + network name (16)
+ *
+ */
+#define OT_CRYPTO_PBDKF2_MAX_SALT_SIZE 30
+
+/**
  * Initialize the Crypto module.
  *
  */
@@ -160,12 +258,12 @@
  *       This API is only used by OT core when `OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE` is enabled.
  *
  */
-otError otPlatCryptoImportKey(otCryptoKeyRef *     aKeyRef,
+otError otPlatCryptoImportKey(otCryptoKeyRef      *aKeyRef,
                               otCryptoKeyType      aKeyType,
                               otCryptoKeyAlgorithm aKeyAlgorithm,
                               int                  aKeyUsage,
                               otCryptoKeyStorage   aKeyPersistence,
-                              const uint8_t *      aKey,
+                              const uint8_t       *aKey,
                               size_t               aKeyLen);
 
 /**
@@ -242,7 +340,7 @@
  * Start HMAC operation.
  *
  * @param[in]  aContext           Context for HMAC operation.
- * @param[in]  aKey               Key material to be used for for HMAC operation.
+ * @param[in]  aKey               Key material to be used for HMAC operation.
  *
  * @retval OT_ERROR_NONE          Successfully started HMAC operation.
  * @retval OT_ERROR_FAILED        Failed to start HMAC operation.
@@ -364,9 +462,9 @@
  *
  */
 otError otPlatCryptoHkdfExpand(otCryptoContext *aContext,
-                               const uint8_t *  aInfo,
+                               const uint8_t   *aInfo,
                                uint16_t         aInfoLength,
-                               uint8_t *        aOutputKey,
+                               uint8_t         *aOutputKey,
                                uint16_t         aOutputKeyLength);
 
 /**
@@ -381,8 +479,8 @@
  * @retval OT_ERROR_FAILED        HKDF Extract failed.
  *
  */
-otError otPlatCryptoHkdfExtract(otCryptoContext *  aContext,
-                                const uint8_t *    aSalt,
+otError otPlatCryptoHkdfExtract(otCryptoContext   *aContext,
+                                const uint8_t     *aSalt,
                                 uint16_t           aSaltLength,
                                 const otCryptoKey *aInputKey);
 
@@ -480,16 +578,181 @@
 /**
  * Fills a given buffer with cryptographically secure random bytes.
  *
- * @param[out] aBuffer  A pointer to a buffer to fill with the random bytes.
- * @param[in]  aSize    Size of buffer (number of bytes to fill).
+ * @param[out] aBuffer            A pointer to a buffer to fill with the random bytes.
+ * @param[in]  aSize              Size of buffer (number of bytes to fill).
  *
- * @retval OT_ERROR_NONE     Successfully filled buffer with random values.
- * @retval OT_ERROR_FAILED   Operation failed.
+ * @retval OT_ERROR_NONE          Successfully filled buffer with random values.
+ * @retval OT_ERROR_FAILED        Operation failed.
  *
  */
 otError otPlatCryptoRandomGet(uint8_t *aBuffer, uint16_t aSize);
 
 /**
+ * Generate and populate the output buffer with a new ECDSA key-pair.
+ *
+ * @param[out] aKeyPair           A pointer to an ECDSA key-pair structure to store the generated key-pair.
+ *
+ * @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.
+ *
+ */
+otError otPlatCryptoEcdsaGenerateKey(otPlatCryptoEcdsaKeyPair *aKeyPair);
+
+/**
+ * Get the associated public key from the input context.
+ *
+ * @param[in]  aKeyPair           A pointer to an ECDSA key-pair structure 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.
+ *
+ */
+otError otPlatCryptoEcdsaGetPublicKey(const otPlatCryptoEcdsaKeyPair *aKeyPair, otPlatCryptoEcdsaPublicKey *aPublicKey);
+
+/**
+ * Calculate the ECDSA signature for a hashed message using the private key from the input context.
+ *
+ * This method uses the deterministic digital signature generation procedure from RFC 6979.
+ *
+ * @param[in]  aKeyPair           A pointer to an ECDSA key-pair structure 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.
+ *
+ */
+otError otPlatCryptoEcdsaSign(const otPlatCryptoEcdsaKeyPair *aKeyPair,
+                              const otPlatCryptoSha256Hash   *aHash,
+                              otPlatCryptoEcdsaSignature     *aSignature);
+
+/**
+ * Use the key from the input context to verify the ECDSA signature of a hashed message.
+ *
+ * @param[in]  aPublicKey         A pointer to an ECDSA public key structure where the public key for signature
+ *                                verification 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.
+ *
+ */
+otError otPlatCryptoEcdsaVerify(const otPlatCryptoEcdsaPublicKey *aPublicKey,
+                                const otPlatCryptoSha256Hash     *aHash,
+                                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.
+ * @param[in]     aPasswordLen       Length of password.
+ * @param[in]     aSalt              Salt to use when generating key.
+ * @param[in]     aSaltLen           Length of salt.
+ * @param[in]     aIterationCounter  Iteration count.
+ * @param[in]     aKeyLen            Length of generated key in bytes.
+ * @param[out]    aKey               A pointer to the generated key.
+ *
+ */
+void otPlatCryptoPbkdf2GenerateKey(const uint8_t *aPassword,
+                                   uint16_t       aPasswordLen,
+                                   const uint8_t *aSalt,
+                                   uint16_t       aSaltLen,
+                                   uint32_t       aIterationCounter,
+                                   uint16_t       aKeyLen,
+                                   uint8_t       *aKey);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/platform/diag.h b/include/openthread/platform/diag.h
index 9dee346..6f65f82 100644
--- a/include/openthread/platform/diag.h
+++ b/include/openthread/platform/diag.h
@@ -57,6 +57,16 @@
  */
 
 /**
+ * This enumeration defines the gpio modes.
+ *
+ */
+typedef enum
+{
+    OT_GPIO_MODE_INPUT  = 0, ///< Input mode without pull resistor.
+    OT_GPIO_MODE_OUTPUT = 1, ///< Output mode.
+} otGpioMode;
+
+/**
  * This function processes a factory diagnostics command line.
  *
  * The output of this function (the content written to @p aOutput) MUST terminate with `\0` and the `\0` is within the
@@ -75,8 +85,8 @@
  */
 otError otPlatDiagProcess(otInstance *aInstance,
                           uint8_t     aArgsLength,
-                          char *      aArgs[],
-                          char *      aOutput,
+                          char       *aArgs[],
+                          char       *aOutput,
                           size_t      aOutputMaxLen);
 
 /**
@@ -130,6 +140,165 @@
 void otPlatDiagAlarmCallback(otInstance *aInstance);
 
 /**
+ * This function sets the gpio value.
+ *
+ * @param[in]  aGpio   The gpio number.
+ * @param[in]  aValue  true to set the gpio to high level, or false otherwise.
+ *
+ * @retval OT_ERROR_NONE             Successfully set the gpio.
+ * @retval OT_ERROR_FAILED           A platform error occurred while setting the gpio.
+ * @retval OT_ERROR_INVALID_ARGS     @p aGpio is not supported.
+ * @retval OT_ERROR_INVALID_STATE    Diagnostic mode was not enabled or @p aGpio is not configured as output.
+ * @retval OT_ERROR_NOT_IMPLEMENTED  This function is not implemented or configured on the platform.
+ *
+ */
+otError otPlatDiagGpioSet(uint32_t aGpio, bool aValue);
+
+/**
+ * This function gets the gpio value.
+ *
+ * @param[in]   aGpio   The gpio number.
+ * @param[out]  aValue  A pointer where to put gpio value.
+ *
+ * @retval OT_ERROR_NONE             Successfully got the gpio value.
+ * @retval OT_ERROR_FAILED           A platform error occurred while getting the gpio value.
+ * @retval OT_ERROR_INVALID_ARGS     @p aGpio is not supported or @p aValue is NULL.
+ * @retval OT_ERROR_INVALID_STATE    Diagnostic mode was not enabled or @p aGpio is not configured as input.
+ * @retval OT_ERROR_NOT_IMPLEMENTED  This function is not implemented or configured on the platform.
+ *
+ */
+otError otPlatDiagGpioGet(uint32_t aGpio, bool *aValue);
+
+/**
+ * This function sets the gpio mode.
+ *
+ * @param[in]   aGpio   The gpio number.
+ * @param[out]  aMode   The gpio mode.
+ *
+ * @retval OT_ERROR_NONE             Successfully set the gpio mode.
+ * @retval OT_ERROR_FAILED           A platform error occurred while setting the gpio mode.
+ * @retval OT_ERROR_INVALID_ARGS     @p aGpio or @p aMode is not supported.
+ * @retval OT_ERROR_INVALID_STATE    Diagnostic mode was not enabled.
+ * @retval OT_ERROR_NOT_IMPLEMENTED  This function is not implemented or configured on the platform.
+ *
+ */
+otError otPlatDiagGpioSetMode(uint32_t aGpio, otGpioMode aMode);
+
+/**
+ * This function gets the gpio mode.
+ *
+ * @param[in]   aGpio   The gpio number.
+ * @param[out]  aMode   A pointer where to put gpio mode.
+ *
+ * @retval OT_ERROR_NONE             Successfully got the gpio mode.
+ * @retval OT_ERROR_FAILED           Mode returned by the platform is not implemented in OpenThread or a platform error
+ *                                   occurred while getting the gpio mode.
+ * @retval OT_ERROR_INVALID_ARGS     @p aGpio is not supported or @p aMode is NULL.
+ * @retval OT_ERROR_INVALID_STATE    Diagnostic mode was not enabled.
+ * @retval OT_ERROR_NOT_IMPLEMENTED  This function is not implemented or configured on the platform.
+ *
+ */
+otError otPlatDiagGpioGetMode(uint32_t aGpio, otGpioMode *aMode);
+
+/**
+ * Set the radio raw power setting for diagnostics module.
+ *
+ * @param[in] aInstance               The OpenThread instance structure.
+ * @param[in] aRawPowerSetting        A pointer to the raw power setting byte array.
+ * @param[in] aRawPowerSettingLength  The length of the @p aRawPowerSetting.
+ *
+ * @retval OT_ERROR_NONE             Successfully set the raw power setting.
+ * @retval OT_ERROR_INVALID_ARGS     The @p aRawPowerSetting is NULL or the @p aRawPowerSettingLength is too long.
+ * @retval OT_ERROR_NOT_IMPLEMENTED  This method is not implemented.
+ *
+ */
+otError otPlatDiagRadioSetRawPowerSetting(otInstance    *aInstance,
+                                          const uint8_t *aRawPowerSetting,
+                                          uint16_t       aRawPowerSettingLength);
+
+/**
+ * Get the radio raw power setting for diagnostics module.
+ *
+ * @param[in]      aInstance               The OpenThread instance structure.
+ * @param[out]     aRawPowerSetting        A pointer to the raw power setting byte array.
+ * @param[in,out]  aRawPowerSettingLength  On input, a pointer to the size of @p aRawPowerSetting.
+ *                                         On output, a pointer to the length of the raw power setting data.
+ *
+ * @retval OT_ERROR_NONE             Successfully set the raw power setting.
+ * @retval OT_ERROR_INVALID_ARGS     The @p aRawPowerSetting or @p aRawPowerSettingLength is NULL or
+ *                                   @aRawPowerSettingLength is too short.
+ * @retval OT_ERROR_NOT_FOUND        The raw power setting is not set.
+ * @retval OT_ERROR_NOT_IMPLEMENTED  This method is not implemented.
+ *
+ */
+otError otPlatDiagRadioGetRawPowerSetting(otInstance *aInstance,
+                                          uint8_t    *aRawPowerSetting,
+                                          uint16_t   *aRawPowerSettingLength);
+
+/**
+ * Enable/disable the platform layer to use the raw power setting set by `otPlatDiagRadioSetRawPowerSetting()`.
+ *
+ * @param[in]  aInstance The OpenThread instance structure.
+ * @param[in]  aEnable   TRUE to enable or FALSE to disable the raw power setting.
+ *
+ * @retval OT_ERROR_NONE             Successfully enabled/disabled the raw power setting.
+ * @retval OT_ERROR_NOT_IMPLEMENTED  This method is not implemented.
+ *
+ */
+otError otPlatDiagRadioRawPowerSettingEnable(otInstance *aInstance, bool aEnable);
+
+/**
+ * Start/stop the platform layer to transmit continuous carrier wave.
+ *
+ * @param[in]  aInstance The OpenThread instance structure.
+ * @param[in]  aEnable   TRUE to enable or FALSE to disable the platform layer to transmit continuous carrier wave.
+ *
+ * @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 method is not implemented.
+ *
+ */
+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.
+ * @param[in]      aChannel                The radio channel.
+ * @param[out]     aTargetPower            The target power in 0.01 dBm.
+ * @param[out]     aActualPower            The actual power in 0.01 dBm.
+ * @param[out]     aRawPowerSetting        A pointer to the raw power setting byte array.
+ * @param[in,out]  aRawPowerSettingLength  On input, a pointer to the size of @p aRawPowerSetting.
+ *                                         On output, a pointer to the length of the raw power setting data.
+ *
+ * @retval  OT_ERROR_NONE             Successfully got the target power.
+ * @retval  OT_ERROR_INVALID_ARGS     The @p aChannel is invalid, @aTargetPower, @p aActualPower, @p aRawPowerSetting or
+ *                                    @p aRawPowerSettingLength is NULL or @aRawPowerSettingLength is too short.
+ * @retval  OT_ERROR_NOT_FOUND        The power settings for the @p aChannel was not found.
+ * @retval  OT_ERROR_NOT_IMPLEMENTED  This method is not implemented.
+ *
+ */
+otError otPlatDiagRadioGetPowerSettings(otInstance *aInstance,
+                                        uint8_t     aChannel,
+                                        int16_t    *aTargetPower,
+                                        int16_t    *aActualPower,
+                                        uint8_t    *aRawPowerSetting,
+                                        uint16_t   *aRawPowerSettingLength);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/platform/dns.h b/include/openthread/platform/dns.h
new file mode 100644
index 0000000..9e84fbe
--- /dev/null
+++ b/include/openthread/platform/dns.h
@@ -0,0 +1,114 @@
+/*
+ *  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 defines the platform DNS interface.
+ *
+ */
+
+#ifndef OPENTHREAD_PLATFORM_DNS_H_
+#define OPENTHREAD_PLATFORM_DNS_H_
+
+#include <openthread/instance.h>
+#include <openthread/message.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @addtogroup plat-dns
+ *
+ * @brief
+ *   This module includes the platform abstraction for sending recursive DNS query to upstream DNS servers.
+ *
+ * @{
+ *
+ */
+
+/**
+ * This opaque type represents an upstream DNS query transaction.
+ *
+ */
+typedef struct otPlatDnsUpstreamQuery otPlatDnsUpstreamQuery;
+
+/**
+ * Starts an upstream query transaction.
+ *
+ * - In success case (and errors represented by DNS protocol messages), the platform is expected to call
+ *   `otPlatDnsUpstreamQueryDone`.
+ * - The OpenThread core may cancel a (possibly timeout) query transaction by calling
+ *   `otPlatDnsCancelUpstreamQuery`, the platform must not call `otPlatDnsUpstreamQueryDone` on a
+ *   cancelled transaction.
+ *
+ * @param[in] aInstance  The OpenThread instance structure.
+ * @param[in] aTxn       A pointer to the opaque DNS query transaction object.
+ * @param[in] aQuery     A message buffer of the DNS payload that should be sent to upstream DNS server.
+ *
+ */
+void otPlatDnsStartUpstreamQuery(otInstance *aInstance, otPlatDnsUpstreamQuery *aTxn, const otMessage *aQuery);
+
+/**
+ * Cancels a transaction of upstream query.
+ *
+ * The platform must call `otPlatDnsUpstreamQueryDone` to release the resources.
+ *
+ * @param[in] aInstance  The OpenThread instance structure.
+ * @param[in] aTxn       A pointer to the opaque DNS query transaction object.
+ *
+ */
+void otPlatDnsCancelUpstreamQuery(otInstance *aInstance, otPlatDnsUpstreamQuery *aTxn);
+
+/**
+ * The platform calls this function to finish DNS query.
+ *
+ * The transaction will be released, so the platform must not call on the same transaction twice. This function passes
+ * the ownership of `aResponse` to OpenThread stack.
+ *
+ * Platform can pass a nullptr to close a transaction without a response.
+ *
+ * @param[in] aInstance  The OpenThread instance structure.
+ * @param[in] aTxn       A pointer to the opaque DNS query transaction object.
+ * @param[in] aResponse  A message buffer of the DNS response payload or `nullptr` to close a transaction without a
+ *                       response.
+ *
+ */
+extern void otPlatDnsUpstreamQueryDone(otInstance *aInstance, otPlatDnsUpstreamQuery *aTxn, otMessage *aResponse);
+
+/**
+ * @}
+ *
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/openthread/platform/infra_if.h b/include/openthread/platform/infra_if.h
index 9242213..5d2c50f 100644
--- a/include/openthread/platform/infra_if.h
+++ b/include/openthread/platform/infra_if.h
@@ -87,7 +87,7 @@
  */
 otError otPlatInfraIfSendIcmp6Nd(uint32_t            aInfraIfIndex,
                                  const otIp6Address *aDestAddress,
-                                 const uint8_t *     aBuffer,
+                                 const uint8_t      *aBuffer,
                                  uint16_t            aBufferLength);
 
 /**
@@ -106,10 +106,10 @@
  *        address and the IP Hop Limit MUST be 255.
  *
  */
-extern void otPlatInfraIfRecvIcmp6Nd(otInstance *        aInstance,
+extern void otPlatInfraIfRecvIcmp6Nd(otInstance         *aInstance,
                                      uint32_t            aInfraIfIndex,
                                      const otIp6Address *aSrcAddress,
-                                     const uint8_t *     aBuffer,
+                                     const uint8_t      *aBuffer,
                                      uint16_t            aBufferLength);
 
 /**
@@ -134,6 +134,35 @@
 extern otError otPlatInfraIfStateChanged(otInstance *aInstance, uint32_t aInfraIfIndex, bool aIsRunning);
 
 /**
+ * Send a request to discover the NAT64 prefix on the infrastructure interface with @p aInfraIfIndex.
+ *
+ * OpenThread will call this method periodically to monitor the presence or change of NAT64 prefix.
+ *
+ * @param[in]  aInfraIfIndex  The index of the infrastructure interface to discover the NAT64 prefix.
+ *
+ * @retval  OT_ERROR_NONE    Successfully request NAT64 prefix discovery.
+ * @retval  OT_ERROR_FAILED  Failed to request NAT64 prefix discovery.
+ *
+ */
+otError otPlatInfraIfDiscoverNat64Prefix(uint32_t aInfraIfIndex);
+
+/**
+ * The infra interface driver calls this method to notify OpenThread that
+ * the discovery of NAT64 prefix is done.
+ *
+ * This method is expected to be invoked after calling otPlatInfraIfDiscoverNat64Prefix.
+ * If no NAT64 prefix is discovered, @p aIp6Prefix shall point to an empty prefix with zero length.
+ *
+ * @param[in]  aInstance      The OpenThread instance structure.
+ * @param[in]  aInfraIfIndex  The index of the infrastructure interface on which the NAT64 prefix is discovered.
+ * @param[in]  aIp6Prefix     A pointer to NAT64 prefix.
+ *
+ */
+extern void otPlatInfraIfDiscoverNat64PrefixDone(otInstance        *aInstance,
+                                                 uint32_t           aInfraIfIndex,
+                                                 const otIp6Prefix *aIp6Prefix);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/platform/radio.h b/include/openthread/platform/radio.h
index b95acd5..89e17be 100644
--- a/include/openthread/platform/radio.h
+++ b/include/openthread/platform/radio.h
@@ -68,13 +68,20 @@
 
 enum
 {
-    OT_RADIO_FRAME_MAX_SIZE    = 127,    ///< aMaxPHYPacketSize (IEEE 802.15.4-2006)
-    OT_RADIO_FRAME_MIN_SIZE    = 3,      ///< Minimal size of frame FCS + CONTROL
+    OT_RADIO_FRAME_MAX_SIZE = 127, ///< aMaxPHYPacketSize (IEEE 802.15.4-2006)
+    OT_RADIO_FRAME_MIN_SIZE = 3,   ///< Minimal size of frame FCS + CONTROL
+
     OT_RADIO_SYMBOLS_PER_OCTET = 2,      ///< 2.4 GHz IEEE 802.15.4-2006
     OT_RADIO_BIT_RATE          = 250000, ///< 2.4 GHz IEEE 802.15.4 (bits per second)
     OT_RADIO_BITS_PER_OCTET    = 8,      ///< Number of bits per octet
 
-    OT_RADIO_SYMBOL_TIME   = ((OT_RADIO_BITS_PER_OCTET / OT_RADIO_SYMBOLS_PER_OCTET) * 1000000) / OT_RADIO_BIT_RATE,
+    // Per IEEE 802.15.4-2015, 12.3.3 Symbol rate:
+    // The O-QPSK PHY symbol rate shall be 25 ksymbol/s when operating in the 868 MHz band and 62.5 ksymbol/s when
+    // operating in the 780 MHz, 915 MHz, 2380 MHz, or 2450 MHz band
+    OT_RADIO_SYMBOL_RATE = 62500, ///< The O-QPSK PHY symbol rate when operating in the 780MHz, 915MHz, 2380MHz, 2450MHz
+    OT_RADIO_SYMBOL_TIME = 1000000 * 1 / OT_RADIO_SYMBOL_RATE, ///< Symbol duration time in unit of microseconds
+    OT_RADIO_TEN_SYMBOLS_TIME = 10 * OT_RADIO_SYMBOL_TIME,     ///< Time for 10 symbols in unit of microseconds
+
     OT_RADIO_LQI_NONE      = 0,   ///< LQI measurement not supported
     OT_RADIO_RSSI_INVALID  = 127, ///< Invalid or unknown RSSI value
     OT_RADIO_POWER_INVALID = 127, ///< Invalid or unknown power value
@@ -264,13 +271,34 @@
         struct
         {
             const otMacKeyMaterial *mAesKey;  ///< The key material used for AES-CCM frame security.
-            otRadioIeInfo *         mIeInfo;  ///< The pointer to the Header IE(s) related information.
+            otRadioIeInfo          *mIeInfo;  ///< The pointer to the Header IE(s) related information.
             uint32_t                mTxDelay; ///< The delay time for this transmission (based on `mTxDelayBaseTime`).
             uint32_t                mTxDelayBaseTime; ///< The base time for the transmission delay.
             uint8_t mMaxCsmaBackoffs; ///< Maximum number of backoffs attempts before declaring CCA failure.
             uint8_t mMaxFrameRetries; ///< Maximum number of retries allowed after a transmission failure.
 
             /**
+             * The RX channel after frame TX is done (after all frame retries - ack received, or timeout, or abort).
+             *
+             * Radio platforms can choose to fully ignore this. OT stack will make sure to call `otPlatRadioReceive()`
+             * with the desired RX channel after a frame TX is done and signaled in `otPlatRadioTxDone()` callback.
+             * Radio platforms that don't provide `OT_RADIO_CAPS_TRANSMIT_RETRIES` must always ignore this.
+             *
+             * This is intended for situations where there may be delay in interactions between OT stack and radio, as
+             * an example this is used in RCP/host architecture to make sure RCP switches to PAN channel more quickly.
+             * In particular, this can help with CSL tx to a sleepy child, where the child may use a different channel
+             * for CSL than the PAN channel. After frame tx, we want the radio/RCP to go back to the PAN channel
+             * quickly to ensure that parent does not miss tx from child afterwards, e.g., child responding to the
+             * earlier CSL transmitted frame from parent using PAN channel while radio still staying on CSL channel.
+             *
+             * The switch to the RX channel MUST happen after the frame TX is fully done, i.e., after all retries and
+             * when ack is received (when "Ack Request" flag is set on the TX frame) or ack timeout. Note that ack is
+             * expected on the same channel that frame is sent on.
+             *
+             */
+            uint8_t mRxChannelAfterTxDone;
+
+            /**
              * Indicates whether frame counter and CSL IEs are properly updated in the header.
              *
              * If the platform layer does not provide `OT_RADIO_CAPS_TRANSMIT_SEC` capability, it can ignore this flag.
@@ -306,8 +334,7 @@
             /**
              * The timestamp when the frame was received in microseconds.
              *
-             * The value SHALL be the time when the SFD was received when TIME_SYNC or CSL is enabled.
-             * Otherwise, the time when the MAC frame was fully received is also acceptable.
+             * The value SHALL be the time when the SFD was received.
              *
              */
             uint64_t mTimestamp;
@@ -536,7 +563,7 @@
 otError otPlatRadioSetCcaEnergyDetectThreshold(otInstance *aInstance, int8_t aThreshold);
 
 /**
- * Get the external FEM's Rx LNA gain in dBm.
+ * Gets the external FEM's Rx LNA gain in dBm.
  *
  * @param[in]  aInstance  The OpenThread instance structure.
  * @param[out] aGain     The external FEM's Rx LNA gain in dBm.
@@ -549,7 +576,7 @@
 otError otPlatRadioGetFemLnaGain(otInstance *aInstance, int8_t *aGain);
 
 /**
- * Set the external FEM's Rx LNA gain in dBm.
+ * Sets the external FEM's Rx LNA gain in dBm.
  *
  * @param[in] aInstance  The OpenThread instance structure.
  * @param[in] aGain      The external FEM's Rx LNA gain in dBm.
@@ -594,7 +621,7 @@
  * @param[in]   aKeyType     Key Type used.
  *
  */
-void otPlatRadioSetMacKey(otInstance *            aInstance,
+void otPlatRadioSetMacKey(otInstance             *aInstance,
                           uint8_t                 aKeyIdMode,
                           uint8_t                 aKeyId,
                           const otMacKeyMaterial *aPrevKey,
@@ -614,6 +641,17 @@
 void otPlatRadioSetMacFrameCounter(otInstance *aInstance, uint32_t aMacFrameCounter);
 
 /**
+ * This method sets the current MAC frame counter value only if the new given value is larger than the current value.
+ *
+ * This function is used when radio provides `OT_RADIO_CAPS_TRANSMIT_SEC` capability.
+ *
+ * @param[in]   aInstance         A pointer to an OpenThread instance.
+ * @param[in]   aMacFrameCounter  The MAC frame counter value.
+ *
+ */
+void otPlatRadioSetMacFrameCounterIfLarger(otInstance *aInstance, uint32_t aMacFrameCounter);
+
+/**
  * Get the current estimated time (in microseconds) of the radio chip.
  *
  * This microsecond timer must be a free-running timer. The timer must continue to advance with microsecond precision
@@ -969,7 +1007,7 @@
 uint32_t otPlatRadioGetSupportedChannelMask(otInstance *aInstance);
 
 /**
- * Get the radio preferred channel mask that the device prefers to form on.
+ * Gets the radio preferred channel mask that the device prefers to form on.
  *
  * @param[in]  aInstance   The OpenThread instance structure.
  *
@@ -1032,7 +1070,7 @@
  * @retval  kErrorNone           Successfully enabled or disabled CSL.
  *
  */
-otError otPlatRadioEnableCsl(otInstance *        aInstance,
+otError otPlatRadioEnableCsl(otInstance         *aInstance,
                              uint32_t            aCslPeriod,
                              otShortAddress      aShortAddr,
                              const otExtAddress *aExtAddr);
@@ -1096,6 +1134,7 @@
  *
  * @retval  OT_ERROR_FAILED           Other platform specific errors.
  * @retval  OT_ERROR_NONE             Successfully set region code.
+ * @retval  OT_ERROR_NOT_IMPLEMENTED  The feature is not implemented.
  *
  */
 otError otPlatRadioSetRegion(otInstance *aInstance, uint16_t aRegionCode);
@@ -1112,6 +1151,7 @@
  * @retval  OT_ERROR_INVALID_ARGS     @p aRegionCode is nullptr.
  * @retval  OT_ERROR_FAILED           Other platform specific errors.
  * @retval  OT_ERROR_NONE             Successfully got region code.
+ * @retval  OT_ERROR_NOT_IMPLEMENTED  The feature is not implemented.
  *
  */
 otError otPlatRadioGetRegion(otInstance *aInstance, uint16_t *aRegionCode);
@@ -1134,14 +1174,109 @@
  * @retval  OT_ERROR_INVALID_ARGS    @p aExtAddress is `NULL`.
  * @retval  OT_ERROR_NOT_FOUND       The Initiator indicated by @p aShortAddress is not found when trying to clear.
  * @retval  OT_ERROR_NO_BUFS         No more Initiator can be supported.
+ * @retval  OT_ERROR_NOT_IMPLEMENTED The feature is not implemented.
  *
  */
-otError otPlatRadioConfigureEnhAckProbing(otInstance *        aInstance,
+otError otPlatRadioConfigureEnhAckProbing(otInstance         *aInstance,
                                           otLinkMetrics       aLinkMetrics,
                                           otShortAddress      aShortAddress,
                                           const otExtAddress *aExtAddress);
 
 /**
+ * Add a calibrated power of the specified channel to the power calibration table.
+ *
+ * @note This API is an optional radio platform API. It's up to the platform layer to implement it.
+ *
+ * The @p aActualPower is the actual measured output power when the parameters of the radio hardware modules
+ * are set to the @p aRawPowerSetting.
+ *
+ * The raw power setting is an opaque byte array. OpenThread doesn't define the format of the raw power setting.
+ * Its format is radio hardware related and it should be defined by the developers in the platform radio driver.
+ * For example, if the radio hardware contains both the radio chip and the FEM chip, the raw power setting can be
+ * a combination of the radio power register and the FEM gain value.
+ *
+ * @param[in] aInstance               The OpenThread instance structure.
+ * @param[in] aChannel                The radio channel.
+ * @param[in] aActualPower            The actual power in 0.01dBm.
+ * @param[in] aRawPowerSetting        A pointer to the raw power setting byte array.
+ * @param[in] aRawPowerSettingLength  The length of the @p aRawPowerSetting.
+ *
+ * @retval OT_ERROR_NONE             Successfully added the calibrated power to the power calibration table.
+ * @retval OT_ERROR_NO_BUFS          No available entry in the power calibration table.
+ * @retval OT_ERROR_INVALID_ARGS     The @p aChannel, @p aActualPower or @p aRawPowerSetting is invalid or the
+ *                                   @p aActualPower already exists in the power calibration table.
+ * @retval OT_ERROR_NOT_IMPLEMENTED  This feature is not implemented.
+ *
+ */
+otError otPlatRadioAddCalibratedPower(otInstance    *aInstance,
+                                      uint8_t        aChannel,
+                                      int16_t        aActualPower,
+                                      const uint8_t *aRawPowerSetting,
+                                      uint16_t       aRawPowerSettingLength);
+
+/**
+ * Clear all calibrated powers from the power calibration table.
+ *
+ * @note This API is an optional radio platform API. It's up to the platform layer to implement it.
+ *
+ * @param[in]  aInstance   The OpenThread instance structure.
+ *
+ * @retval OT_ERROR_NONE             Successfully cleared all calibrated powers from the power calibration table.
+ * @retval OT_ERROR_NOT_IMPLEMENTED  This feature is not implemented.
+ *
+ */
+otError otPlatRadioClearCalibratedPowers(otInstance *aInstance);
+
+/**
+ * Set the target power for the given channel.
+ *
+ * @note This API is an optional radio platform API. It's up to the platform layer to implement it.
+ *       If this API is implemented, the function `otPlatRadioSetTransmitPower()` should be disabled.
+ *
+ * The radio driver should set the actual output power to be less than or equal to the target power and as close
+ * as possible to the target power.
+ *
+ * @param[in]  aInstance     The OpenThread instance structure.
+ * @param[in]  aChannel      The radio channel.
+ * @param[in]  aTargetPower  The target power in 0.01dBm. Passing `INT16_MAX` will disable this channel to use the
+ *                           target power.
+ *
+ * @retval  OT_ERROR_NONE             Successfully set the target power.
+ * @retval  OT_ERROR_INVALID_ARGS     The @p aChannel or @p aTargetPower is invalid.
+ * @retval  OT_ERROR_NOT_IMPLEMENTED  The feature is not implemented.
+ *
+ */
+otError otPlatRadioSetChannelTargetPower(otInstance *aInstance, uint8_t aChannel, int16_t aTargetPower);
+
+/**
+ * Get the raw power setting for the given channel.
+ *
+ * @note OpenThread `src/core/utils` implements a default implementation of the API `otPlatRadioAddCalibratedPower()`,
+ *       `otPlatRadioClearCalibratedPowers()` and `otPlatRadioSetChannelTargetPower()`. This API is provided by
+ *       the default implementation to get the raw power setting for the given channel. If the platform doesn't
+ *       use the default implementation, it can ignore this API.
+ *
+ * Platform radio layer should parse the raw power setting based on the radio layer defined format and set the
+ * parameters of each radio hardware module.
+ *
+ * @param[in]      aInstance               The OpenThread instance structure.
+ * @param[in]      aChannel                The radio channel.
+ * @param[out]     aRawPowerSetting        A pointer to the raw power setting byte array.
+ * @param[in,out]  aRawPowerSettingLength  On input, a pointer to the size of @p aRawPowerSetting.
+ *                                         On output, a pointer to the length of the raw power setting data.
+ *
+ * @retval  OT_ERROR_NONE          Successfully got the target power.
+ * @retval  OT_ERROR_INVALID_ARGS  The @p aChannel is invalid, @p aRawPowerSetting or @p aRawPowerSettingLength is NULL
+ *                                 or @aRawPowerSettingLength is too short.
+ * @retval  OT_ERROR_NOT_FOUND     The raw power setting for the @p aChannel was not found.
+ *
+ */
+extern otError otPlatRadioGetRawPowerSetting(otInstance *aInstance,
+                                             uint8_t     aChannel,
+                                             uint8_t    *aRawPowerSetting,
+                                             uint16_t   *aRawPowerSettingLength);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/platform/settings.h b/include/openthread/platform/settings.h
index 2a4892b..a9ca05d 100644
--- a/include/openthread/platform/settings.h
+++ b/include/openthread/platform/settings.h
@@ -72,6 +72,8 @@
     OT_SETTINGS_KEY_SRP_CLIENT_INFO      = 0x000c, ///< The SRP client info (selected SRP server address).
     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/spi-slave.h b/include/openthread/platform/spi-slave.h
index 3b84667..551d701 100644
--- a/include/openthread/platform/spi-slave.h
+++ b/include/openthread/platform/spi-slave.h
@@ -79,7 +79,7 @@
  * @returns  TRUE if after this call returns the platform should invoke the process callback `aProcessCallback`,
  *           FALSE if there is nothing to process and no need to invoke the process callback.
  */
-typedef bool (*otPlatSpiSlaveTransactionCompleteCallback)(void *   aContext,
+typedef bool (*otPlatSpiSlaveTransactionCompleteCallback)(void    *aContext,
                                                           uint8_t *aOutputBuf,
                                                           uint16_t aOutputBufLen,
                                                           uint8_t *aInputBuf,
@@ -115,7 +115,7 @@
  */
 otError otPlatSpiSlaveEnable(otPlatSpiSlaveTransactionCompleteCallback aCompleteCallback,
                              otPlatSpiSlaveTransactionProcessCallback  aProcessCallback,
-                             void *                                    aContext);
+                             void                                     *aContext);
 
 /**
  * Shutdown and disable the SPI slave interface.
diff --git a/include/openthread/platform/toolchain.h b/include/openthread/platform/toolchain.h
index 5610985..d8b0308 100644
--- a/include/openthread/platform/toolchain.h
+++ b/include/openthread/platform/toolchain.h
@@ -110,6 +110,24 @@
  *
  */
 
+/**
+ * @def OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK
+ *
+ * This macro specifies that a function or method takes `printf` style arguments and should be type-checked against
+ * a format string.
+ *
+ * This macro must be added after the function/method declaration. For example:
+ *
+ *    `void MyPrintf(void *aObject, const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);`
+ *
+ * The two argument index values indicate format string and first argument to check against it. They start at index 1
+ * for the first parameter in a function and at index 2 for the first parameter in a method.
+ *
+ * @param[in] aFmtIndex    The argument index of the format string.
+ * @param[in] aStartIndex  The argument index of the first argument to check against the format string.
+ *
+ */
+
 // =========== TOOLCHAIN SELECTION : START ===========
 
 #if defined(__GNUC__) || defined(__clang__) || defined(__CC_ARM) || defined(__TI_ARM__)
@@ -122,6 +140,9 @@
 #define OT_TOOL_PACKED_END __attribute__((packed))
 #define OT_TOOL_WEAK __attribute__((weak))
 
+#define OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(aFmtIndex, aStartIndex) \
+    __attribute__((format(printf, aFmtIndex, aStartIndex)))
+
 #elif defined(__ICCARM__) || defined(__ICC8051__)
 
 // http://supp.iar.com/FilesPublic/UPDINFO/004916/arm/doc/EWARM_DevelopmentGuide.ENU.pdf
@@ -133,6 +154,8 @@
 #define OT_TOOL_PACKED_END
 #define OT_TOOL_WEAK __weak
 
+#define OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(aFmtIndex, aStartIndex)
+
 #elif defined(__SDCC)
 
 // Structures are packed by default in sdcc, as it primarily targets 8-bit MCUs.
@@ -142,6 +165,8 @@
 #define OT_TOOL_PACKED_END
 #define OT_TOOL_WEAK
 
+#define OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(aFmtIndex, aStartIndex)
+
 #else
 
 #error "Error: No valid Toolchain specified"
@@ -153,6 +178,8 @@
 #define OT_TOOL_PACKED_END
 #define OT_TOOL_WEAK
 
+#define OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(aFmtIndex, aStartIndex)
+
 #endif
 
 // =========== TOOLCHAIN SELECTION : END ===========
diff --git a/include/openthread/platform/trel.h b/include/openthread/platform/trel.h
index d4e0475..6515b98 100644
--- a/include/openthread/platform/trel.h
+++ b/include/openthread/platform/trel.h
@@ -178,8 +178,8 @@
  * @param[in] aDestSockAddr    The destination socket address.
  *
  */
-void otPlatTrelSend(otInstance *      aInstance,
-                    const uint8_t *   aUdpPayload,
+void otPlatTrelSend(otInstance       *aInstance,
+                    const uint8_t    *aUdpPayload,
                     uint16_t          aUdpPayloadLen,
                     const otSockAddr *aDestSockAddr);
 
diff --git a/include/openthread/platform/udp.h b/include/openthread/platform/udp.h
index 345f159..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);
@@ -125,7 +125,7 @@
  * @retval  OT_ERROR_FAILED Failed to join the multicast group.
  *
  */
-otError otPlatUdpJoinMulticastGroup(otUdpSocket *       aUdpSocket,
+otError otPlatUdpJoinMulticastGroup(otUdpSocket        *aUdpSocket,
                                     otNetifIdentifier   aNetifIdentifier,
                                     const otIp6Address *aAddress);
 
@@ -142,7 +142,7 @@
  * @retval  OT_ERROR_FAILED Failed to leave the multicast group.
  *
  */
-otError otPlatUdpLeaveMulticastGroup(otUdpSocket *       aUdpSocket,
+otError otPlatUdpLeaveMulticastGroup(otUdpSocket        *aUdpSocket,
                                      otNetifIdentifier   aNetifIdentifier,
                                      const otIp6Address *aAddress);
 
diff --git a/include/openthread/server.h b/include/openthread/server.h
index 0d177c7..3ce1117 100644
--- a/include/openthread/server.h
+++ b/include/openthread/server.h
@@ -94,7 +94,7 @@
  * @sa otServerRegister
  *
  */
-otError otServerRemoveService(otInstance *   aInstance,
+otError otServerRemoveService(otInstance    *aInstance,
                               uint32_t       aEnterpriseNumber,
                               const uint8_t *aServiceData,
                               uint8_t        aServiceDataLength);
diff --git a/include/openthread/sntp.h b/include/openthread/sntp.h
index ca61f0a..34b26c2 100644
--- a/include/openthread/sntp.h
+++ b/include/openthread/sntp.h
@@ -93,10 +93,10 @@
  * @param[in]  aContext    A pointer to arbitrary context information.
  *
  */
-otError otSntpClientQuery(otInstance *          aInstance,
-                          const otSntpQuery *   aQuery,
+otError otSntpClientQuery(otInstance           *aInstance,
+                          const otSntpQuery    *aQuery,
                           otSntpResponseHandler aHandler,
-                          void *                aContext);
+                          void                 *aContext);
 
 /**
  * This function sets the unix era number.
diff --git a/include/openthread/srp_client.h b/include/openthread/srp_client.h
index bb291d4..beaa062 100644
--- a/include/openthread/srp_client.h
+++ b/include/openthread/srp_client.h
@@ -74,8 +74,8 @@
  */
 typedef struct otSrpClientHostInfo
 {
-    const char *         mName;         ///< Host name (label) string (NULL if not yet set).
-    const otIp6Address * mAddresses;    ///< Array of host IPv6 addresses (NULL if not set or auto address is enabled).
+    const char          *mName;         ///< Host name (label) string (NULL if not yet set).
+    const otIp6Address  *mAddresses;    ///< Array of host IPv6 addresses (NULL if not set or auto address is enabled).
     uint8_t              mNumAddresses; ///< Number of IPv6 addresses in `mAddresses` array.
     bool                 mAutoAddress;  ///< Indicates whether auto address mode is enabled or not.
     otSrpClientItemState mState;        ///< Host info state.
@@ -88,28 +88,34 @@
  * and stay constant after an instance of this structure is passed to OpenThread from `otSrpClientAddService()` or
  * `otSrpClientRemoveService()`.
  *
+ * The `mState`, `mData`, `mNext` fields are used/managed by OT core only. Their value is ignored when an instance of
+ * `otSrpClientService` is passed in `otSrpClientAddService()` or `otSrpClientRemoveService()` or other functions. The
+ * caller does not need to set these fields.
+ *
+ * The `mLease` and `mKeyLease` fields specify the desired lease and key lease intervals for this service. Zero value
+ * indicates that the interval is unspecified and then the default lease or key lease intervals from
+ * `otSrpClientGetLeaseInterval()` and `otSrpClientGetKeyLeaseInterval()` are used for this service. If the key lease
+ * interval (whether set explicitly or determined from the default) is shorter than the lease interval for a service,
+ * SRP client will re-use the lease interval value for key lease interval as well. For example, if in service `mLease`
+ * is explicitly set to 2 days and `mKeyLease` is set to zero and default key lease is set to 1 day, then when
+ * registering this service, the requested key lease for this service is also set to 2 days.
+ *
  */
 typedef struct otSrpClientService
 {
-    const char *         mName;          ///< The service name labels (e.g., "_chip._udp", not the full domain name).
-    const char *         mInstanceName;  ///< The service instance name label (not the full name).
-    const char *const *  mSubTypeLabels; ///< Array of service sub-type labels (must end with `NULL` or can be `NULL`).
-    const otDnsTxtEntry *mTxtEntries;    ///< Array of TXT entries (number of entries is given by `mNumTxtEntries`).
-    uint16_t             mPort;          ///< The service port number.
-    uint16_t             mPriority;      ///< The service priority.
-    uint16_t             mWeight;        ///< The service weight.
-    uint8_t              mNumTxtEntries; ///< Number of entries in the `mTxtEntries` array.
-
-    /**
-     * @note The following fields are used/managed by OT core only. Their values do not matter and are ignored when an
-     * instance of `otSrpClientService` is passed in `otSrpClientAddService()` or `otSrpClientRemoveService()`. The
-     * user should not modify these fields.
-     *
-     */
-
-    otSrpClientItemState       mState; ///< Service state (managed by OT core).
-    uint32_t                   mData;  ///< Internal data (used by OT core).
-    struct otSrpClientService *mNext;  ///< Pointer to next entry in a linked-list (managed by OT core).
+    const char                *mName;          ///< The service labels (e.g., "_mt._udp", not the full domain name).
+    const char                *mInstanceName;  ///< The service instance name label (not the full name).
+    const char *const         *mSubTypeLabels; ///< Array of sub-type labels (must end with `NULL` or can be `NULL`).
+    const otDnsTxtEntry       *mTxtEntries;    ///< Array of TXT entries (`mNumTxtEntries` gives num of entries).
+    uint16_t                   mPort;          ///< The service port number.
+    uint16_t                   mPriority;      ///< The service priority.
+    uint16_t                   mWeight;        ///< The service weight.
+    uint8_t                    mNumTxtEntries; ///< Number of entries in the `mTxtEntries` array.
+    otSrpClientItemState       mState;         ///< Service state (managed by OT core).
+    uint32_t                   mData;          ///< Internal data (used by OT core).
+    struct otSrpClientService *mNext;          ///< Pointer to next entry in a linked-list (managed by OT core).
+    uint32_t                   mLease;         ///< Desired lease interval in sec - zero to use default.
+    uint32_t                   mKeyLease;      ///< Desired key lease interval in sec - zero to use default.
 } otSrpClientService;
 
 /**
@@ -169,9 +175,9 @@
  */
 typedef void (*otSrpClientCallback)(otError                    aError,
                                     const otSrpClientHostInfo *aHostInfo,
-                                    const otSrpClientService * aServices,
-                                    const otSrpClientService * aRemovedServices,
-                                    void *                     aContext);
+                                    const otSrpClientService  *aServices,
+                                    const otSrpClientService  *aRemovedServices,
+                                    void                      *aContext);
 
 /**
  * This function pointer type defines the callback used by SRP client to notify user when it is auto-started or stopped.
@@ -270,12 +276,26 @@
  * Config option `OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_DEFAULT_MODE` specifies the default auto-start mode (whether
  * it is enabled or disabled at the start of OT stack).
  *
- * When auto-start is enabled, the SRP client will monitor the Thread Network Data for SRP Server Service entries
- * and automatically start and stop the client when an SRP server is detected.
+ * When auto-start is enabled, the SRP client will monitor the Thread Network Data to discover SRP servers and select
+ * the preferred server and automatically start and stop the client when an SRP server is detected.
  *
- * If multiple SRP servers are found, a random one will be selected. If the selected SRP server is no longer
- * detected (not longer present in the Thread Network Data), the SRP client will be stopped and then it may switch
- * to another SRP server (if available).
+ * There are three categories of Network Data entries indicating presence of SRP sever. They are preferred in the
+ * following order:
+ *
+ *   1) Preferred unicast entries where server address is included in the service data. If there are multiple options,
+ *      the one with numerically lowest IPv6 address is preferred.
+ *
+ *   2) Anycast entries each having a seq number. A larger sequence number in the sense specified by Serial Number
+ *      Arithmetic logic in RFC-1982 is considered more recent and therefore preferred. The largest seq number using
+ *      serial number arithmetic is preferred if it is well-defined (i.e., the seq number is larger than all other
+ *      seq numbers). If it is not well-defined, then the numerically largest seq number is preferred.
+ *
+ *   3) Unicast entries where the server address info is included in server data. If there are multiple options, the
+ *      one with numerically lowest IPv6 address is preferred.
+ *
+ * When there is a change in the Network Data entries, client will check that the currently selected server is still
+ * present in the Network Data and is still the preferred one. Otherwise the client will switch to the new preferred
+ * server or stop if there is none.
  *
  * When the SRP client is explicitly started through a successful call to `otSrpClientStart()`, the given SRP server
  * address in `otSrpClientStart()` will continue to be used regardless of the state of auto-start mode and whether the
@@ -345,7 +365,9 @@
 void otSrpClientSetTtl(otInstance *aInstance, uint32_t aTtl);
 
 /**
- * This function gets the lease interval used in SRP update requests.
+ * This function gets the default lease interval used in SRP update requests.
+ *
+ * The default interval is used only for `otSrpClientService` instances with `mLease` set to zero.
  *
  * Note that this is the lease duration requested by the SRP client. The server may choose to accept a different lease
  * interval.
@@ -358,7 +380,9 @@
 uint32_t otSrpClientGetLeaseInterval(otInstance *aInstance);
 
 /**
- * This function sets the lease interval used in SRP update requests.
+ * This function sets the default lease interval used in SRP update requests.
+ *
+ * The default interval is used only for `otSrpClientService` instances with `mLease` set to zero.
  *
  * Changing the lease interval does not impact the accepted lease interval of already registered services/host-info.
  * It only affects any future SRP update messages (i.e., adding new services and/or refreshes of the existing services).
@@ -371,7 +395,9 @@
 void otSrpClientSetLeaseInterval(otInstance *aInstance, uint32_t aInterval);
 
 /**
- * This function gets the key lease interval used in SRP update requests.
+ * This function gets the default key lease interval used in SRP update requests.
+ *
+ * The default interval is used only for `otSrpClientService` instances with `mKeyLease` set to zero.
  *
  * Note that this is the lease duration requested by the SRP client. The server may choose to accept a different lease
  * interval.
@@ -384,7 +410,9 @@
 uint32_t otSrpClientGetKeyLeaseInterval(otInstance *aInstance);
 
 /**
- * This function sets the key lease interval used in SRP update requests.
+ * This function sets the default key lease interval used in SRP update requests.
+ *
+ * The default interval is used only for `otSrpClientService` instances with `mKeyLease` set to zero.
  *
  * Changing the lease interval does not impact the accepted lease interval of already registered services/host-info.
  * It only affects any future SRP update messages (i.e., adding new services and/or refreshes of existing services).
diff --git a/include/openthread/srp_server.h b/include/openthread/srp_server.h
index fba7174..c3dd36b 100644
--- a/include/openthread/srp_server.h
+++ b/include/openthread/srp_server.h
@@ -132,14 +132,14 @@
 };
 
 /**
- * Represents the state of an SRP server
+ * This enumeration represents the state of the SRP server.
  *
  */
 typedef enum
 {
     OT_SRP_SERVER_STATE_DISABLED = 0, ///< The SRP server is disabled.
-    OT_SRP_SERVER_STATE_RUNNING  = 1, ///< The SRP server is running.
-    OT_SRP_SERVER_STATE_STOPPED  = 2, ///< The SRP server is stopped.
+    OT_SRP_SERVER_STATE_RUNNING  = 1, ///< The SRP server is enabled and running.
+    OT_SRP_SERVER_STATE_STOPPED  = 2, ///< The SRP server is enabled but stopped.
 } otSrpServerState;
 
 /**
@@ -302,6 +302,8 @@
 /**
  * This function enables/disables the SRP server.
  *
+ * On a Border Router, it is recommended to use `otSrpServerSetAutoEnableMode()` instead.
+ *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  * @param[in]  aEnabled   A boolean to enable/disable the SRP server.
  *
@@ -309,6 +311,41 @@
 void otSrpServerSetEnabled(otInstance *aInstance, bool aEnabled);
 
 /**
+ * This function enables/disables the auto-enable mode on SRP server.
+ *
+ * This function requires `OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE` feature.
+ *
+ * When this mode is enabled, the Border Routing Manager controls if/when to enable or disable the SRP server.
+ * SRP sever is auto-enabled if/when Border Routing is started and it is done with the initial prefix and route
+ * configurations (when the OMR and on-link prefixes are determined, advertised in emitted Router Advertisement message
+ * on infrastructure side and published in the Thread Network Data). The SRP server is auto-disabled if/when BR is
+ * stopped (e.g., if the infrastructure network interface is brought down or if BR gets detached).
+ *
+ * This mode can be disabled by a `otSrpServerSetAutoEnableMode()` call with @p aEnabled set to `false` or if the SRP
+ * server is explicitly enabled or disabled by a call to `otSrpServerSetEnabled()` function. Disabling auto-enable mode
+ * using `otSrpServerSetAutoEnableMode(false)` will not change the current state of SRP sever (e.g., if it is enabled
+ * it stays enabled).
+ *
+ * @param[in] aInstance   A pointer to an OpenThread instance.
+ * @param[in] aEnabled    A boolean to enable/disable the auto-enable mode.
+ *
+ */
+void otSrpServerSetAutoEnableMode(otInstance *aInstance, bool aEnabled);
+
+/**
+ * This function indicates whether the auto-enable mode is enabled or disabled.
+ *
+ * This function requires `OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE` feature.
+ *
+ * @param[in]  aInstance  A pointer to an OpenThread instance.
+ *
+ * @retval TRUE   The auto-enable mode is enabled.
+ * @retval FALSE  The auto-enable mode is disabled.
+ *
+ */
+bool otSrpServerIsAutoEnableMode(otInstance *aInstance);
+
+/**
  * This function returns SRP server TTL configuration.
  *
  * @param[in]   aInstance   A pointer to an OpenThread instance.
@@ -392,9 +429,9 @@
  *
  */
 typedef void (*otSrpServerServiceUpdateHandler)(otSrpServerServiceUpdateId aId,
-                                                const otSrpServerHost *    aHost,
+                                                const otSrpServerHost     *aHost,
                                                 uint32_t                   aTimeout,
-                                                void *                     aContext);
+                                                void                      *aContext);
 
 /**
  * This function sets the SRP service updates handler on SRP server.
@@ -405,9 +442,9 @@
  *                              May be NULL if not used.
  *
  */
-void otSrpServerSetServiceUpdateHandler(otInstance *                    aInstance,
+void otSrpServerSetServiceUpdateHandler(otInstance                     *aInstance,
                                         otSrpServerServiceUpdateHandler aServiceHandler,
-                                        void *                          aContext);
+                                        void                           *aContext);
 
 /**
  * This function reports the result of processing a SRP update to the SRP server.
@@ -500,7 +537,7 @@
  * @returns  A pointer to the next service or NULL if there is no more services.
  *
  */
-const otSrpServerService *otSrpServerHostGetNextService(const otSrpServerHost *   aHost,
+const otSrpServerService *otSrpServerHostGetNextService(const otSrpServerHost    *aHost,
                                                         const otSrpServerService *aService);
 
 /**
@@ -535,11 +572,11 @@
  * @returns  A pointer to the next matching service or NULL if no matching service could be found.
  *
  */
-const otSrpServerService *otSrpServerHostFindNextService(const otSrpServerHost *   aHost,
+const otSrpServerService *otSrpServerHostFindNextService(const otSrpServerHost    *aHost,
                                                          const otSrpServerService *aPrevService,
                                                          otSrpServerServiceFlags   aFlags,
-                                                         const char *              aServiceName,
-                                                         const char *              aInstanceName);
+                                                         const char               *aServiceName,
+                                                         const char               *aInstanceName);
 
 /**
  * This function indicates whether or not the SRP service has been deleted.
diff --git a/include/openthread/tcp.h b/include/openthread/tcp.h
index 6cc11f7..3b63bc3 100644
--- a/include/openthread/tcp.h
+++ b/include/openthread/tcp.h
@@ -63,7 +63,7 @@
 typedef struct otLinkedBuffer
 {
     struct otLinkedBuffer *mNext;   ///< Pointer to the next linked buffer in the chain, or NULL if it is the end.
-    const uint8_t *        mData;   ///< Pointer to data referenced by this linked buffer.
+    const uint8_t         *mData;   ///< Pointer to data referenced by this linked buffer.
     size_t                 mLength; ///< Length of this linked buffer (number of bytes).
 } otLinkedBuffer;
 
@@ -249,7 +249,7 @@
     } mTcb;
 
     struct otTcpEndpoint *mNext;    ///< A pointer to the next TCP endpoint (internal use only)
-    void *                mContext; ///< A pointer to application-specific context
+    void                 *mContext; ///< A pointer to application-specific context
 
     otTcpEstablished      mEstablishedCallback;      ///< "Established" callback function
     otTcpSendDone         mSendDoneCallback;         ///< "Send done" callback function
@@ -279,7 +279,7 @@
     otTcpReceiveAvailable mReceiveAvailableCallback; ///< "Receive available" callback function
     otTcpDisconnected     mDisconnectedCallback;     ///< "Disconnected" callback function
 
-    void * mReceiveBuffer;     ///< Pointer to memory provided to the system for the TCP receive buffer
+    void  *mReceiveBuffer;     ///< Pointer to memory provided to the system for the TCP receive buffer
     size_t mReceiveBufferSize; ///< Size of memory provided to the system for the TCP receive buffer
 } otTcpEndpointInitializeArgs;
 
@@ -294,7 +294,7 @@
  * select a smaller buffer size.
  *
  */
-#define OT_TCP_RECEIVE_BUFFER_SIZE_FEW_HOPS 2599
+#define OT_TCP_RECEIVE_BUFFER_SIZE_FEW_HOPS 2598
 
 /**
  * @def OT_TCP_RECEIVE_BUFFER_SIZE_MANY_HOPS
@@ -306,7 +306,7 @@
  * so), then it may be advisable to select a large buffer size manually.
  *
  */
-#define OT_TCP_RECEIVE_BUFFER_SIZE_MANY_HOPS 4158
+#define OT_TCP_RECEIVE_BUFFER_SIZE_MANY_HOPS 4157
 
 /**
  * Initializes a TCP endpoint.
@@ -325,8 +325,8 @@
  * @retval OT_ERROR_FAILED  Failed to open the TCP endpoint.
  *
  */
-otError otTcpEndpointInitialize(otInstance *                       aInstance,
-                                otTcpEndpoint *                    aEndpoint,
+otError otTcpEndpointInitialize(otInstance                        *aInstance,
+                                otTcpEndpoint                     *aEndpoint,
                                 const otTcpEndpointInitializeArgs *aArgs);
 
 /**
@@ -401,11 +401,11 @@
 /**
  * Records the remote host and port for this connection.
  *
- * By default TCP Fast Open is used. This means that this function merely
- * records the remote host and port, and that the TCP connection establishment
- * handshake only happens on the first call to otTcpSendByReference(). TCP Fast
- * Open can be explicitly disabled using @p aFlags, in which case the TCP
- * connection establishment handshake is initiated immediately.
+ * Caller must wait for `otTcpEstablished` callback indicating that TCP
+ * connection establishment handshake is done before it can start sending data
+ * e.g., calling `otTcpSendByReference()`.
+ *
+ * The TCP Fast Open is not yet supported and @p aFlags is ignored.
  *
  * @param[in]  aEndpoint  A pointer to the TCP endpoint structure to connect.
  * @param[in]  aSockName  The IP address and port of the host to which to connect.
@@ -624,9 +624,9 @@
  * @returns  Description of how to handle the incoming connection.
  *
  */
-typedef otTcpIncomingConnectionAction (*otTcpAcceptReady)(otTcpListener *   aListener,
+typedef otTcpIncomingConnectionAction (*otTcpAcceptReady)(otTcpListener    *aListener,
                                                           const otSockAddr *aPeer,
-                                                          otTcpEndpoint **  aAcceptInto);
+                                                          otTcpEndpoint   **aAcceptInto);
 
 /**
  * This callback indicates that the TCP connection is now ready for two-way
@@ -670,11 +670,11 @@
     union
     {
         uint8_t mSize[OT_TCP_LISTENER_TCB_SIZE_BASE + OT_TCP_LISTENER_TCB_NUM_PTR * sizeof(void *)];
-        void *  mAlign;
+        void   *mAlign;
     } mTcbListen;
 
     struct otTcpListener *mNext;    ///< A pointer to the next TCP listener (internal use only)
-    void *                mContext; ///< A pointer to application-specific context
+    void                 *mContext; ///< A pointer to application-specific context
 
     otTcpAcceptReady mAcceptReadyCallback; ///< "Accept ready" callback function
     otTcpAcceptDone  mAcceptDoneCallback;  ///< "Accept done" callback function
@@ -709,8 +709,8 @@
  * @retval OT_ERROR_FAILED  Failed to open the TCP listener.
  *
  */
-otError otTcpListenerInitialize(otInstance *                       aInstance,
-                                otTcpListener *                    aListener,
+otError otTcpListenerInitialize(otInstance                        *aInstance,
+                                otTcpListener                     *aListener,
                                 const otTcpListenerInitializeArgs *aArgs);
 
 /**
diff --git a/include/openthread/tcp_ext.h b/include/openthread/tcp_ext.h
index 5c0ddc6..0258ac2 100644
--- a/include/openthread/tcp_ext.h
+++ b/include/openthread/tcp_ext.h
@@ -88,10 +88,10 @@
  */
 typedef struct otTcpCircularSendBuffer
 {
-    const uint8_t *mDataBuffer;   ///< Pointer to data in the circular send buffer
-    size_t         mCapacity;     ///< Length of the circular send buffer
-    size_t         mStartIndex;   ///< Index of the first valid byte in the send buffer
-    size_t         mCapacityUsed; ///< Number of bytes stored in the send buffer
+    uint8_t *mDataBuffer;   ///< Pointer to data in the circular send buffer
+    size_t   mCapacity;     ///< Length of the circular send buffer
+    size_t   mStartIndex;   ///< Index of the first valid byte in the send buffer
+    size_t   mCapacityUsed; ///< Number of bytes stored in the send buffer
 
     otLinkedBuffer mSendLinks[2];
     uint8_t        mFirstSendLinkIndex;
@@ -108,6 +108,15 @@
 void otTcpCircularSendBufferInitialize(otTcpCircularSendBuffer *aSendBuffer, void *aDataBuffer, size_t aCapacity);
 
 /**
+ * This enumeration defines flags passed to @p otTcpCircularSendBufferWrite.
+ *
+ */
+enum
+{
+    OT_TCP_CIRCULAR_SEND_BUFFER_WRITE_MORE_TO_COME = 1 << 0,
+};
+
+/**
  * Sends out data on a TCP endpoint, using the provided TCP circular send
  * buffer to manage buffering.
  *
@@ -136,15 +145,17 @@
  * @param[in]   aLength      The length of the data pointed to by @p aData to copy into the TCP circular send buffer.
  * @param[out]  aWritten     Populated with the amount of data copied into the send buffer, which might be less than
  *                           @p aLength if the send buffer reaches capacity.
+ * @param[in]   aFlags       Flags specifying options for this operation (see enumeration above).
  *
- * @returns OT_ERROR_NONE    Successfully copied data into the send buffer and sent it on the TCP endpoint.
- * @returns OT_ERROR_FAILED  Failed to send out data on the TCP endpoint.
+ * @retval OT_ERROR_NONE    Successfully copied data into the send buffer and sent it on the TCP endpoint.
+ * @retval OT_ERROR_FAILED  Failed to send out data on the TCP endpoint.
  */
-otError otTcpCircularSendBufferWrite(otTcpEndpoint *          aEndpoint,
+otError otTcpCircularSendBufferWrite(otTcpEndpoint           *aEndpoint,
                                      otTcpCircularSendBuffer *aSendBuffer,
-                                     void *                   aData,
+                                     const void              *aData,
                                      size_t                   aLength,
-                                     size_t *                 aWritten);
+                                     size_t                  *aWritten,
+                                     uint32_t                 aFlags);
 
 /**
  * Performs circular-send-buffer-specific handling in the otTcpForwardProgress
@@ -173,9 +184,9 @@
  *
  * @param[in]  aSendBuffer  A pointer to the TCP circular send buffer whose amount of free space to return.
  *
- * @return The amount of free space in the send buffer.
+ * @returns The amount of free space in the send buffer.
  */
-size_t otTcpCircularSendBufferFreeSpace(otTcpCircularSendBuffer *aSendBuffer);
+size_t otTcpCircularSendBufferGetFreeSpace(const otTcpCircularSendBuffer *aSendBuffer);
 
 /**
  * Forcibly discards all data in the circular send buffer.
@@ -205,6 +216,37 @@
 otError otTcpCircularSendBufferDeinitialize(otTcpCircularSendBuffer *aSendBuffer);
 
 /**
+ * Context structure to use with mbedtls_ssl_set_bio.
+ */
+typedef struct otTcpEndpointAndCircularSendBuffer
+{
+    otTcpEndpoint           *mEndpoint;
+    otTcpCircularSendBuffer *mSendBuffer;
+} otTcpEndpointAndCircularSendBuffer;
+
+/**
+ * Non-blocking send callback to pass to mbedtls_ssl_set_bio.
+ *
+ * @param[in]  aCtx  A pointer to an otTcpEndpointAndCircularSendBuffer.
+ * @param[in]  aBuf  The data to add to the send buffer.
+ * @param[in]  aLen  The amount of data to add to the send buffer.
+ *
+ * @returns The number of bytes sent, or an mbedtls error code.
+ */
+int otTcpMbedTlsSslSendCallback(void *aCtx, const unsigned char *aBuf, size_t aLen);
+
+/**
+ * Non-blocking receive callback to pass to mbedtls_ssl_set_bio.
+ *
+ * @param[in]   aCtx  A pointer to an otTcpEndpointAndCircularSendBuffer.
+ * @param[out]  aBuf  The buffer into which to receive data.
+ * @param[in]   aLen  The maximum amount of data that can be received.
+ *
+ * @returns The number of bytes received, or an mbedtls error code.
+ */
+int otTcpMbedTlsSslRecvCallback(void *aCtx, unsigned char *aBuf, size_t aLen);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/thread.h b/include/openthread/thread.h
index 04cb747..4bde02c 100644
--- a/include/openthread/thread.h
+++ b/include/openthread/thread.h
@@ -90,15 +90,18 @@
 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
     uint8_t      mLinkQualityIn;        ///< Link Quality In
     int8_t       mAverageRssi;          ///< Average RSSI
     int8_t       mLastRssi;             ///< Last observed RSSI
+    uint8_t      mLinkMargin;           ///< Link Margin
     uint16_t     mFrameErrorRate;       ///< Frame error rate (0xffff->100%). Requires error tracking feature.
     uint16_t     mMessageErrorRate;     ///< (IPv6) msg error rate (0xffff->100%). Requires error tracking feature.
+    uint16_t     mVersion;              ///< Thread version of the neighbor
     bool         mRxOnWhenIdle : 1;     ///< rx-on-when-idle
     bool         mFullThreadDevice : 1; ///< Full Thread Device
     bool         mFullNetworkData : 1;  ///< Full Network Data
@@ -138,6 +141,14 @@
     uint8_t      mAge;                 ///< Time last heard
     bool         mAllocated : 1;       ///< Router ID allocated or not
     bool         mLinkEstablished : 1; ///< Link established with Router ID or not
+    uint8_t      mVersion;             ///< Thread version
+
+    /**
+     * Parent CSL parameters are only relevant when OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE is enabled.
+     *
+     */
+    uint8_t mCslClockAccuracy; ///< CSL clock accuracy, in ± ppm
+    uint8_t mCslUncertainty;   ///< CSL uncertainty, in ±10 us
 } otRouterInfo;
 
 /**
@@ -168,6 +179,19 @@
     uint16_t mBetterPartitionAttachAttempts; ///< Number of attempts to attach to a better partition.
 
     /**
+     * Role time tracking.
+     *
+     * When uptime feature is enabled (OPENTHREAD_CONFIG_UPTIME_ENABLE = 1) time spent in each MLE role is tracked.
+     *
+     */
+    uint64_t mDisabledTime; ///< Number of milliseconds device has been in OT_DEVICE_ROLE_DISABLED role.
+    uint64_t mDetachedTime; ///< Number of milliseconds device has been in OT_DEVICE_ROLE_DETACHED role.
+    uint64_t mChildTime;    ///< Number of milliseconds device has been in OT_DEVICE_ROLE_CHILD role.
+    uint64_t mRouterTime;   ///< Number of milliseconds device has been in OT_DEVICE_ROLE_ROUTER role.
+    uint64_t mLeaderTime;   ///< Number of milliseconds device has been in OT_DEVICE_ROLE_LEADER role.
+    uint64_t mTrackedTime;  ///< Number of milliseconds tracked by previous counters.
+
+    /**
      * Number of times device changed its parent.
      *
      * A parent change can happen if device detaches from its current parent and attaches to a different one, or even
@@ -241,6 +265,8 @@
 /**
  * This function starts a Thread Discovery scan.
  *
+ * @note A successful call to this function enables the rx-on-when-idle mode for the entire scan procedure.
+ *
  * @param[in]  aInstance              A pointer to an OpenThread instance.
  * @param[in]  aScanChannels          A bit vector indicating which channels to scan (e.g. OT_CHANNEL_11_MASK).
  * @param[in]  aPanId                 The PAN ID filter (set to Broadcast PAN to disable filter).
@@ -256,13 +282,13 @@
  * @retval OT_ERROR_BUSY           Thread Discovery Scan is already in progress.
  *
  */
-otError otThreadDiscover(otInstance *             aInstance,
+otError otThreadDiscover(otInstance              *aInstance,
                          uint32_t                 aScanChannels,
                          uint16_t                 aPanId,
                          bool                     aJoiner,
                          bool                     aEnableEui64Filtering,
                          otHandleActiveScanResult aCallback,
-                         void *                   aCallbackContext);
+                         void                    *aCallbackContext);
 
 /**
  * This function determines if an MLE Thread Discovery is currently in progress.
@@ -289,7 +315,7 @@
  * @retval OT_ERROR_INVALID_ARGS Invalid AdvData.
  *
  */
-otError otThreadSetJoinerAdvertisement(otInstance *   aInstance,
+otError otThreadSetJoinerAdvertisement(otInstance    *aInstance,
                                        uint32_t       aOui,
                                        const uint8_t *aAdvData,
                                        uint8_t        aAdvDataLength);
@@ -297,7 +323,7 @@
 #define OT_JOINER_ADVDATA_MAX_LENGTH 64 ///< Maximum AdvData Length of Joiner Advertisement
 
 /**
- * Get the Thread Child Timeout used when operating in the Child role.
+ * Gets the Thread Child Timeout (in seconds) used when operating in the Child role.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -309,7 +335,7 @@
 uint32_t otThreadGetChildTimeout(otInstance *aInstance);
 
 /**
- * Set the Thread Child Timeout used when operating in the Child role.
+ * Sets the Thread Child Timeout (in seconds) used when operating in the Child role.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  * @param[in]  aTimeout  The timeout value in seconds.
@@ -320,7 +346,7 @@
 void otThreadSetChildTimeout(otInstance *aInstance, uint32_t aTimeout);
 
 /**
- * Get the IEEE 802.15.4 Extended PAN ID.
+ * Gets the IEEE 802.15.4 Extended PAN ID.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -332,9 +358,9 @@
 const otExtendedPanId *otThreadGetExtendedPanId(otInstance *aInstance);
 
 /**
- * Set the IEEE 802.15.4 Extended PAN ID.
+ * Sets the IEEE 802.15.4 Extended PAN ID.
  *
- * This function can only be called while Thread protocols are disabled.  A successful
+ * @note Can only be called while Thread protocols are disabled. A successful
  * call to this function invalidates the Active and Pending Operational Datasets in
  * non-volatile memory.
  *
@@ -391,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
  *
@@ -451,7 +477,7 @@
 otError otThreadSetNetworkKeyRef(otInstance *aInstance, otNetworkKeyRef aKeyRef);
 
 /**
- * This function returns a pointer to the Thread Routing Locator (RLOC) address.
+ * Gets the Thread Routing Locator (RLOC) address.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -461,7 +487,7 @@
 const otIp6Address *otThreadGetRloc(otInstance *aInstance);
 
 /**
- * This function returns a pointer to the Mesh Local EID address.
+ * Gets the Mesh Local EID address.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -497,7 +523,7 @@
 otError otThreadSetMeshLocalPrefix(otInstance *aInstance, const otMeshLocalPrefix *aMeshLocalPrefix);
 
 /**
- * This function returns the Thread link-local IPv6 address.
+ * Gets the Thread link-local IPv6 address.
  *
  * The Thread link local address is derived using IEEE802.15.4 Extended Address as Interface Identifier.
  *
@@ -509,9 +535,9 @@
 const otIp6Address *otThreadGetLinkLocalIp6Address(otInstance *aInstance);
 
 /**
- * This function returns the Thread Link-Local All Thread Nodes multicast address.
+ * Gets the Thread Link-Local All Thread Nodes multicast address.
  *
- * The address is a link-local Unicast Prefix-Based Multcast Address [RFC 3306], with:
+ * The address is a link-local Unicast Prefix-Based Multicast Address [RFC 3306], with:
  *   - flgs set to 3 (P = 1 and T = 1)
  *   - scop set to 2
  *   - plen set to 64
@@ -526,9 +552,9 @@
 const otIp6Address *otThreadGetLinkLocalAllThreadNodesMulticastAddress(otInstance *aInstance);
 
 /**
- * This function returns the Thread Realm-Local All Thread Nodes multicast address.
+ * Gets the Thread Realm-Local All Thread Nodes multicast address.
  *
- * The address is a realm-local Unicast Prefix-Based Multcast Address [RFC 3306], with:
+ * The address is a realm-local Unicast Prefix-Based Multicast Address [RFC 3306], with:
  *   - flgs set to 3 (P = 1 and T = 1)
  *   - scop set to 3
  *   - plen set to 64
@@ -585,9 +611,9 @@
 otError otThreadSetNetworkName(otInstance *aInstance, const char *aNetworkName);
 
 /**
- * Get the Thread Domain Name.
+ * Gets the Thread Domain Name.
  *
- * This function is only available since Thread 1.2.
+ * @note Available since Thread 1.2.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -599,10 +625,9 @@
 const char *otThreadGetDomainName(otInstance *aInstance);
 
 /**
- * Set the Thread Domain Name.
+ * Sets the Thread Domain Name. Only succeeds when Thread protocols are disabled.
  *
- * This function is only available since Thread 1.2.
- * This function succeeds only when Thread protocols are disabled.
+ * @note Available since Thread 1.2.
  *
  * @param[in]  aInstance     A pointer to an OpenThread instance.
  * @param[in]  aDomainName   A pointer to the Thread Domain Name.
@@ -616,9 +641,11 @@
 otError otThreadSetDomainName(otInstance *aInstance, const char *aDomainName);
 
 /**
- * Set/Clear the Interface Identifier manually specified for the Thread Domain Unicast Address.
+ * Sets or clears the Interface Identifier manually specified for the Thread Domain Unicast Address.
  *
- * This function is only available since Thread 1.2 when `OPENTHREAD_CONFIG_DUA_ENABLE` is enabled.
+ * Available when `OPENTHREAD_CONFIG_DUA_ENABLE` is enabled.
+ *
+ * @note Only available since Thread 1.2.
  *
  * @param[in]  aInstance   A pointer to an OpenThread instance.
  * @param[in]  aIid        A pointer to the Interface Identifier to set or NULL to clear.
@@ -631,9 +658,11 @@
 otError otThreadSetFixedDuaInterfaceIdentifier(otInstance *aInstance, const otIp6InterfaceIdentifier *aIid);
 
 /**
- * Get the Interface Identifier manually specified for the Thread Domain Unicast Address.
+ * Gets the Interface Identifier manually specified for the Thread Domain Unicast Address.
  *
- * This function is only available since Thread 1.2 when `OPENTHREAD_CONFIG_DUA_ENABLE` is enabled.
+ * Available when `OPENTHREAD_CONFIG_DUA_ENABLE` is enabled.
+ *
+ * @note Only available since Thread 1.2.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -645,7 +674,7 @@
 const otIp6InterfaceIdentifier *otThreadGetFixedDuaInterfaceIdentifier(otInstance *aInstance);
 
 /**
- * Get the thrKeySequenceCounter.
+ * Gets the thrKeySequenceCounter.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -657,7 +686,7 @@
 uint32_t otThreadGetKeySequenceCounter(otInstance *aInstance);
 
 /**
- * Set the thrKeySequenceCounter.
+ * Sets the thrKeySequenceCounter.
  *
  * @note This API is reserved for testing and demo purposes only. Changing settings with
  * this API will render a production application non-compliant with the Thread Specification.
@@ -671,7 +700,7 @@
 void otThreadSetKeySequenceCounter(otInstance *aInstance, uint32_t aKeySequenceCounter);
 
 /**
- * Get the thrKeySwitchGuardTime
+ * Gets the thrKeySwitchGuardTime (in hours).
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -683,7 +712,7 @@
 uint32_t otThreadGetKeySwitchGuardTime(otInstance *aInstance);
 
 /**
- * Set the thrKeySwitchGuardTime
+ * Sets the thrKeySwitchGuardTime (in hours).
  *
  * @note This API is reserved for testing and demo purposes only. Changing settings with
  * this API will render a production application non-compliant with the Thread Specification.
@@ -845,7 +874,18 @@
 otError otThreadGetParentLastRssi(otInstance *aInstance, int8_t *aLastRssi);
 
 /**
- * Get the IPv6 counters.
+ * Starts the process for child to search for a better parent while staying attached to its current parent.
+ *
+ * Must be used when device is attached as a child.
+ *
+ * @retval OT_ERROR_NONE           Successfully started the process to search for a better parent.
+ * @retval OT_ERROR_INVALID_STATE  Device role is not child.
+ *
+ */
+otError otThreadSearchForBetterParent(otInstance *aInstance);
+
+/**
+ * Gets the IPv6 counters.
  *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  *
@@ -855,7 +895,7 @@
 const otIpCounters *otThreadGetIp6Counters(otInstance *aInstance);
 
 /**
- * Reset the IPv6 counters.
+ * Resets the IPv6 counters.
  *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  *
@@ -863,7 +903,7 @@
 void otThreadResetIp6Counters(otInstance *aInstance);
 
 /**
- * Get the Thread MLE counters.
+ * Gets the Thread MLE counters.
  *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  *
@@ -873,7 +913,7 @@
 const otMleCounters *otThreadGetMleCounters(otInstance *aInstance);
 
 /**
- * Reset the Thread MLE counters.
+ * Resets the Thread MLE counters.
  *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  *
@@ -883,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.
  *
@@ -892,14 +934,16 @@
 /**
  * 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.
  *
  */
-void otThreadRegisterParentResponseCallback(otInstance *                   aInstance,
+void otThreadRegisterParentResponseCallback(otInstance                    *aInstance,
                                             otThreadParentResponseCallback aCallback,
-                                            void *                         aContext);
+                                            void                          *aContext);
 
 /**
  * This structure represents the Thread Discovery Request data.
@@ -929,9 +973,9 @@
  * @param[in]  aContext   A pointer to callback application-specific context.
  *
  */
-void otThreadSetDiscoveryRequestCallback(otInstance *                     aInstance,
+void otThreadSetDiscoveryRequestCallback(otInstance                      *aInstance,
                                          otThreadDiscoveryRequestCallback aCallback,
-                                         void *                           aContext);
+                                         void                            *aContext);
 
 /**
  * This function pointer type defines the callback to notify the outcome of a `otThreadLocateAnycastDestination()`
@@ -946,7 +990,7 @@
  * @param[in] aRloc16             The RLOC16 of the destination if found, otherwise invalid RLOC16 (0xfffe).
  *
  */
-typedef void (*otThreadAnycastLocatorCallback)(void *              aContext,
+typedef void (*otThreadAnycastLocatorCallback)(void               *aContext,
                                                otError             aError,
                                                const otIp6Address *aMeshLocalAddress,
                                                uint16_t            aRloc16);
@@ -968,10 +1012,10 @@
  * @retval OT_ERROR_NO_BUFS       Out of buffer to prepare and send the request message.
  *
  */
-otError otThreadLocateAnycastDestination(otInstance *                   aInstance,
-                                         const otIp6Address *           aAnycastAddress,
+otError otThreadLocateAnycastDestination(otInstance                    *aInstance,
+                                         const otIp6Address            *aAnycastAddress,
                                          otThreadAnycastLocatorCallback aCallback,
-                                         void *                         aContext);
+                                         void                          *aContext);
 
 /**
  * This function indicates whether an anycast locate request is currently in progress.
@@ -996,9 +1040,9 @@
  * @param[in]  aMlIid        The ML-IID of the ADDR_NTF.ntf message.
  *
  */
-void otThreadSendAddressNotification(otInstance *              aInstance,
-                                     otIp6Address *            aDestination,
-                                     otIp6Address *            aTarget,
+void otThreadSendAddressNotification(otInstance               *aInstance,
+                                     otIp6Address             *aDestination,
+                                     otIp6Address             *aTarget,
                                      otIp6InterfaceIdentifier *aMlIid);
 
 /**
@@ -1015,8 +1059,8 @@
  * @retval OT_ERROR_NO_BUFS        If insufficient message buffers available.
  *
  */
-otError otThreadSendProactiveBackboneNotification(otInstance *              aInstance,
-                                                  otIp6Address *            aTarget,
+otError otThreadSendProactiveBackboneNotification(otInstance               *aInstance,
+                                                  otIp6Address             *aTarget,
                                                   otIp6InterfaceIdentifier *aMlIid,
                                                   uint32_t                  aTimeSinceLastTransaction);
 
@@ -1035,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 ea052de..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
@@ -68,6 +69,7 @@
     uint16_t     mFrameErrorRate;       ///< Frame error rate (0xffff->100%). Requires error tracking feature.
     uint16_t     mMessageErrorRate;     ///< (IPv6) msg error rate (0xffff->100%). Requires error tracking feature.
     uint16_t     mQueuedMessageCnt;     ///< Number of queued messages for the child.
+    uint16_t     mSupervisionInterval;  ///< Supervision interval (in seconds).
     uint8_t      mVersion;              ///< MLE version
     bool         mRxOnWhenIdle : 1;     ///< rx-on-when-idle
     bool         mFullThreadDevice : 1; ///< Full Thread Device
@@ -122,7 +124,7 @@
 } otCacheEntryIterator;
 
 /**
- * Get the maximum number of children currently allowed.
+ * Gets the maximum number of children currently allowed.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
@@ -134,7 +136,7 @@
 uint16_t otThreadGetMaxAllowedChildren(otInstance *aInstance);
 
 /**
- * Set the maximum number of children currently allowed.
+ * Sets the maximum number of children currently allowed.
  *
  * This parameter can only be set when Thread protocol operation has been stopped.
  *
@@ -196,19 +198,76 @@
 otError otThreadSetPreferredRouterId(otInstance *aInstance, uint8_t aRouterId);
 
 /**
- * Get the Thread Leader Weight used when operating in the Leader role.
+ * This enumeration represents the power supply property on a device.
+ *
+ * This is used as a property in `otDeviceProperties` to calculate the leader weight.
+ *
+ */
+typedef enum
+{
+    OT_POWER_SUPPLY_BATTERY           = 0, ///< Battery powered.
+    OT_POWER_SUPPLY_EXTERNAL          = 1, ///< Externally powered (mains-powered).
+    OT_POWER_SUPPLY_EXTERNAL_STABLE   = 2, ///< Stable external power with a battery backup or UPS.
+    OT_POWER_SUPPLY_EXTERNAL_UNSTABLE = 3, ///< Potentially unstable ext power (e.g. light bulb powered via a switch).
+} otPowerSupply;
+
+/**
+ * This structure represents the device properties which are used for calculating the local leader weight on a
+ * device.
+ *
+ * The parameters are set based on device's capability, whether acting as border router, its power supply config, etc.
+ *
+ * `mIsUnstable` indicates operational stability of device and is determined via a vendor specific mechanism. It can
+ * include the following cases:
+ *  - Device internally detects that it loses external power supply more often than usual. What is usual is
+ *    determined by the vendor.
+ *  - Device internally detects that it reboots more often than usual. What is usual is determined by the vendor.
+ *
+ */
+typedef struct otDeviceProperties
+{
+    otPowerSupply mPowerSupply;            ///< Power supply config.
+    bool          mIsBorderRouter : 1;     ///< Whether device is a border router.
+    bool          mSupportsCcm : 1;        ///< Whether device supports CCM (can act as a CCM border router).
+    bool          mIsUnstable : 1;         ///< Operational stability of device (vendor specific).
+    int8_t        mLeaderWeightAdjustment; ///< Weight adjustment. Should be -16 to +16 (clamped otherwise).
+} otDeviceProperties;
+
+/**
+ * Get the current device properties.
+ *
+ * @returns The device properties `otDeviceProperties`.
+ *
+ */
+const otDeviceProperties *otThreadGetDeviceProperties(otInstance *aInstance);
+
+/**
+ * Set the device properties which are then used to determine and set the Leader Weight.
+ *
+ * @param[in]  aInstance           A pointer to an OpenThread instance.
+ * @param[in]  aDeviceProperties   The device properties.
+ *
+ */
+void otThreadSetDeviceProperties(otInstance *aInstance, const otDeviceProperties *aDeviceProperties);
+
+/**
+ * Gets the Thread Leader Weight used when operating in the Leader role.
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  *
  * @returns The Thread Leader Weight value.
  *
  * @sa otThreadSetLeaderWeight
+ * @sa otThreadSetDeviceProperties
  *
  */
 uint8_t otThreadGetLocalLeaderWeight(otInstance *aInstance);
 
 /**
- * Set the Thread Leader Weight used when operating in the Leader role.
+ * Sets the Thread Leader Weight used when operating in the Leader role.
+ *
+ * This function directly sets the Leader Weight to the new value, replacing its previous value (which may have been
+ * determined from the current `otDeviceProperties`).
  *
  * @param[in]  aInstance A pointer to an OpenThread instance.
  * @param[in]  aWeight   The Thread Leader Weight value.
@@ -238,7 +297,7 @@
 void otThreadSetPreferredLeaderPartitionId(otInstance *aInstance, uint32_t aPartitionId);
 
 /**
- * Get the Joiner UDP Port.
+ * Gets the Joiner UDP Port.
  *
  * @param[in] aInstance A pointer to an OpenThread instance.
  *
@@ -250,7 +309,7 @@
 uint16_t otThreadGetJoinerUdpPort(otInstance *aInstance);
 
 /**
- * Set the Joiner UDP Port.
+ * Sets the Joiner UDP Port.
  *
  * @param[in]  aInstance       A pointer to an OpenThread instance.
  * @param[in]  aJoinerUdpPort  The Joiner UDP Port number.
@@ -356,6 +415,35 @@
 void otThreadSetRouterUpgradeThreshold(otInstance *aInstance, uint8_t aThreshold);
 
 /**
+ * Get the MLE_CHILD_ROUTER_LINKS parameter used in the REED role.
+ *
+ * This parameter specifies the max number of neighboring routers with which the device (as an FED)
+ *  will try to establish link.
+ *
+ * @param[in]  aInstance A pointer to an OpenThread instance.
+ *
+ * @returns The MLE_CHILD_ROUTER_LINKS value.
+ *
+ * @sa otThreadSetChildRouterLinks
+ *
+ */
+uint8_t otThreadGetChildRouterLinks(otInstance *aInstance);
+
+/**
+ * Set the MLE_CHILD_ROUTER_LINKS parameter used in the REED role.
+ *
+ * @param[in]  aInstance         A pointer to an OpenThread instance.
+ * @param[in]  aChildRouterLinks The MLE_CHILD_ROUTER_LINKS value.
+ *
+ * @retval OT_ERROR_NONE           Successfully set the value.
+ * @retval OT_ERROR_INVALID_STATE  Thread protocols are enabled.
+ *
+ * @sa otThreadGetChildRouterLinks
+ *
+ */
+otError otThreadSetChildRouterLinks(otInstance *aInstance, uint8_t aChildRouterLinks);
+
+/**
  * Release a Router ID that has been allocated by the device in the Leader role.
  *
  * @note This API is reserved for testing and demo purposes only. Changing settings with
@@ -451,7 +539,7 @@
 void otThreadSetRouterSelectionJitter(otInstance *aInstance, uint8_t aRouterJitter);
 
 /**
- * The function retains diagnostic information for an attached Child by its Child ID or RLOC16.
+ * Gets diagnostic information for an attached Child by its Child ID or RLOC16.
  *
  * @param[in]   aInstance   A pointer to an OpenThread instance.
  * @param[in]   aChildId    The Child ID or RLOC16 for the attached child.
@@ -498,10 +586,10 @@
  * @sa otThreadGetChildInfoByIndex
  *
  */
-otError otThreadGetChildNextIp6Address(otInstance *               aInstance,
+otError otThreadGetChildNextIp6Address(otInstance                *aInstance,
                                        uint16_t                   aChildIndex,
                                        otChildIp6AddressIterator *aIterator,
-                                       otIp6Address *             aAddress);
+                                       otIp6Address              *aAddress);
 
 /**
  * Get the current Router ID Sequence.
@@ -645,7 +733,7 @@
 otError otThreadSetParentPriority(otInstance *aInstance, int8_t aParentPriority);
 
 /**
- * This function gets the maximum number of IP addresses that each MTD child may register with this device as parent.
+ * Gets the maximum number of IP addresses that each MTD child may register with this device as parent.
  *
  * @param[in]  aInstance    A pointer to an OpenThread instance.
  *
@@ -657,11 +745,15 @@
 uint8_t otThreadGetMaxChildIpAddresses(otInstance *aInstance);
 
 /**
- * This function sets/restores the maximum number of IP addresses that each MTD child may register with this
+ * Sets or restores the maximum number of IP addresses that each MTD child may register with this
  * device as parent.
  *
- * @note This API requires `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE`, and is only used by Thread Test Harness
- *       to limit the address registrations of the reference parent in order to test the MTD DUT reaction.
+ * Pass `0` to clear the setting and restore the default.
+ *
+ * Available when `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is enabled.
+ *
+ * @note Only used by Thread Test Harness to limit the address registrations of the reference
+ * parent in order to test the MTD DUT reaction.
  *
  * @param[in]  aInstance        A pointer to an OpenThread instance.
  * @param[in]  aMaxIpAddresses  The maximum number of IP addresses that each MTD child may register with this
@@ -783,6 +875,36 @@
  *
  */
 otError otThreadSetRouterIdRange(otInstance *aInstance, uint8_t aMinRouterId, uint8_t aMaxRouterId);
+
+/**
+ * This function indicates whether or not a Router ID is currently allocated.
+ *
+ * @param[in]  aInstance     A pointer to an OpenThread instance.
+ * @param[in]  aRouterId     The router ID to check.
+ *
+ * @retval TRUE  The @p aRouterId is allocated.
+ * @retval FALSE The @p aRouterId is not allocated.
+ *
+ */
+bool otThreadIsRouterIdAllocated(otInstance *aInstance, uint8_t aRouterId);
+
+/**
+ * This function gets the next hop and path cost towards a given RLOC16 destination.
+ *
+ * This function can be used with either @p aNextHopRloc16 or @p aPathCost being NULL indicating caller does not want
+ * to get the value.
+ *
+ * @param[in]  aInstance       A pointer to an OpenThread instance.
+ * @param[in]  aDesRloct16     The RLOC16 of destination.
+ * @param[out] aNextHopRloc16  A pointer to return RLOC16 of next hop, 0xfffe if no next hop.
+ * @param[out] aPathCost       A pointer to return path cost towards destination.
+ *
+ */
+void otThreadGetNextHopAndPathCost(otInstance *aInstance,
+                                   uint16_t    aDestRloc16,
+                                   uint16_t   *aNextHopRloc16,
+                                   uint8_t    *aPathCost);
+
 /**
  * @}
  *
diff --git a/include/openthread/trel.h b/include/openthread/trel.h
index e2dcbdb..2637350 100644
--- a/include/openthread/trel.h
+++ b/include/openthread/trel.h
@@ -74,30 +74,23 @@
 typedef uint16_t otTrelPeerIterator;
 
 /**
- * This function enables TREL operation.
+ * Enables or disables TREL operation.
  *
- * This function initiates an ongoing DNS-SD browse on the service name "_trel._udp" within the local browsing domain
- * to discover other devices supporting TREL. Device also registers a new service to be advertised using DNS-SD,
- * with the service name is "_trel._udp" indicating its support for TREL. Device is then ready to receive TREL messages
- * from peers.
+ * When @p aEnable is true, this function initiates an ongoing DNS-SD browse on the service name "_trel._udp" within the
+ * local browsing domain to discover other devices supporting TREL. Device also registers a new service to be advertised
+ * using DNS-SD, with the service name is "_trel._udp" indicating its support for TREL. Device is then ready to receive
+ * TREL messages from peers.
+ *
+ * When @p aEnable is false, this function stops the DNS-SD browse on the service name "_trel._udp", stops advertising
+ * TREL DNS-SD service, and clears the TREL peer table.
  *
  * @note By default the OpenThread stack enables the TREL operation on start.
  *
- * @param[in] aInstance   The OpenThread instance.
+ * @param[in]  aInstance  A pointer to an OpenThread instance.
+ * @param[in]  aEnable    A boolean to enable/disable the TREL operation.
  *
  */
-void otTrelEnable(otInstance *aInstance);
-
-/**
- * This function disables TREL operation.
- *
- * This function stops the DNS-SD browse on the service name "_trel._udp", stops advertising TREL DNS-SD service, and
- * clears the TREL peer table.
- *
- * @param[in] aInstance   The OpenThread instance.
- *
- */
-void otTrelDisable(otInstance *aInstance);
+void otTrelSetEnabled(otInstance *aInstance, bool aEnable);
 
 /**
  * This function indicates whether the TREL operation is enabled.
diff --git a/include/openthread/udp.h b/include/openthread/udp.h
index 3b2ffce..5a26813 100644
--- a/include/openthread/udp.h
+++ b/include/openthread/udp.h
@@ -70,7 +70,7 @@
 {
     struct otUdpReceiver *mNext;    ///< A pointer to the next UDP receiver (internal use only).
     otUdpHandler          mHandler; ///< A function pointer to the receiver callback.
-    void *                mContext; ///< A pointer to application-specific context.
+    void                 *mContext; ///< A pointer to application-specific context.
 } otUdpReceiver;
 
 /**
@@ -125,8 +125,8 @@
     otSockAddr          mSockName; ///< The local IPv6 socket address.
     otSockAddr          mPeerName; ///< The peer IPv6 socket address.
     otUdpReceive        mHandler;  ///< A function pointer to the application callback.
-    void *              mContext;  ///< A pointer to application-specific context.
-    void *              mHandle;   ///< A handle to platform's UDP.
+    void               *mContext;  ///< A pointer to application-specific context.
+    void               *mHandle;   ///< A handle to platform's UDP.
     struct otUdpSocket *mNext;     ///< A pointer to the next UDP socket (internal use only).
 } otUdpSocket;
 
@@ -278,11 +278,11 @@
  * @param[in]  aContext   A pointer to application-specific context.
  *
  */
-typedef void (*otUdpForwarder)(otMessage *   aMessage,
+typedef void (*otUdpForwarder)(otMessage    *aMessage,
                                uint16_t      aPeerPort,
                                otIp6Address *aPeerAddr,
                                uint16_t      aSockPort,
-                               void *        aContext);
+                               void         *aContext);
 
 /**
  * Set UDP forward callback to deliver UDP packets to host.
@@ -306,8 +306,8 @@
  * @warning No matter the call success or fail, the message is freed.
  *
  */
-void otUdpForwardReceive(otInstance *        aInstance,
-                         otMessage *         aMessage,
+void otUdpForwardReceive(otInstance         *aInstance,
+                         otMessage          *aMessage,
                          uint16_t            aPeerPort,
                          const otIp6Address *aPeerAddr,
                          uint16_t            aSockPort);
diff --git a/script/bootstrap b/script/bootstrap
index 9a4479c..45f2f75 100755
--- a/script/bootstrap
+++ b/script/bootstrap
@@ -38,7 +38,7 @@
     echo 'Installing pretty tools useful for code contributions...'
 
     # add clang-format and clang-tidy for pretty
-    sudo apt-get --no-install-recommends install -y clang-format-9 clang-tidy-9 || echo 'WARNING: could not install clang-format-9 and clang-tidy-9, which is useful if you plan to contribute C/C++ code to the OpenThread project.'
+    sudo apt-get --no-install-recommends install -y clang-format-14 clang-tidy-14 || echo 'WARNING: could not install clang-format-14 and clang-tidy-14, which is useful if you plan to contribute C/C++ code to the OpenThread project.'
 
     # add yapf for pretty
     python3 -m pip install yapf==0.31.0 || echo 'WARNING: could not install yapf, which is useful if you plan to contribute python code to the OpenThread project.'
@@ -46,8 +46,8 @@
     # add mdv for local size report
     python3 -m pip install mdv || echo 'WARNING: could not install mdv, which is required to post markdown size report for OpenThread.'
 
-    # add shfmt for shell pretty, try brew only because snap does not support home directory not being /home and doesn't work in docker.
-    command -v shfmt || brew install shfmt || echo 'WARNING: could not install shfmt, which is useful if you plan to contribute shell scripts to the OpenThread project.'
+    # add shfmt for shell pretty
+    command -v shfmt || sudo apt-get install shfmt || echo 'WARNING: could not install shfmt, which is useful if you plan to contribute shell scripts to the OpenThread project.'
 }
 
 install_packages_apt()
@@ -66,7 +66,7 @@
     if [ "$PLATFORM" = "Raspbian" ]; then
         sudo apt-get --no-install-recommends install -y binutils-arm-none-eabi gcc-arm-none-eabi gdb-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib
     elif [ "$PLATFORM" = "Ubuntu" ]; then
-        sudo apt-get --no-install-recommends install -y ca-certificates wget
+        sudo apt-get --no-install-recommends install -y bzip2 ca-certificates wget
         (cd /tmp \
             && wget --tries 4 --no-check-certificate --quiet -c https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2020q2/gcc-arm-none-eabi-9-2020-q2-update-"$ARCH"-linux.tar.bz2 \
             && sudo tar xjf gcc-arm-none-eabi-9-2020-q2-update-"$ARCH"-linux.tar.bz2 -C /opt \
@@ -111,11 +111,14 @@
     echo 'Installing pretty tools useful for code contributions...'
 
     # add clang-format for pretty
-    CLANG_FORMAT_VERSION="clang-format version 9"
-    command -v clang-format-9 || (command -v clang-format && (clang-format --version | grep -q "${CLANG_FORMAT_VERSION}")) || {
-        brew install llvm@9
-        sudo ln -s "$(brew --prefix llvm@9)/bin/clang-format" /usr/local/bin/clang-format-9
-    } || echo 'WARNING: could not install llvm@9, which is useful if you plan to contribute C/C++ code to the OpenThread project.'
+    CLANG_FORMAT_VERSION="clang-format version 14"
+    command -v clang-format-14 || (command -v clang-format && (clang-format --version | grep -q "${CLANG_FORMAT_VERSION}")) || {
+        brew install llvm@14
+        sudo ln -s "$(brew --prefix llvm@14)/bin/clang-format" /usr/local/bin/clang-format-14
+        sudo ln -s "$(brew --prefix llvm@14)/bin/clang-tidy" /usr/local/bin/clang-tidy-14
+        sudo ln -s "$(brew --prefix llvm@14)/bin/clang-apply-replacements" /usr/local/bin/clang-apply-replacements-14
+        sudo ln -s "$(brew --prefix llvm@14)/bin/run-clang-tidy" /usr/local/bin/run-clang-tidy-14
+    } || echo 'WARNING: could not install llvm@14, which is useful if you plan to contribute C/C++ code to the OpenThread project.'
 
     # add yapf for pretty
     python3 -m pip install yapf || echo 'Failed to install python code formatter yapf. Install it manually if you need.'
diff --git a/script/check-android-build b/script/check-android-build
deleted file mode 100755
index 6647b9f..0000000
--- a/script/check-android-build
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/bin/bash
-#
-#  Copyright (c) 2018, 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.
-#
-
-#
-# Run this command on parent directory of openthread
-#
-
-set -euxo pipefail
-
-check_targets()
-{
-    for target in "$@"; do
-        make showcommands "${target}"
-        test -x "out/target/product/generic/system/bin/${target}"
-    done
-
-    for target in "$@"; do
-        make "clean-${target}" || true
-    done
-}
-
-check_datetime()
-{
-    local datetime
-
-    datetime="$(date)"
-    cat >openthread-config-datetime.h <<EOF
-#define OPENTHREAD_BUILD_DATETIME "$datetime"
-EOF
-
-    OPENTHREAD_PROJECT_CFLAGS="-I$PWD -DOPENTHREAD_CONFIG_FILE=\\\"openthread-config-datetime.h\\\" \
-        -DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\\\"openthread-core-posix-config.h\\\" \
-        -std=c99" \
-        make showcommands ot-cli
-    grep "$datetime" -ao "out/target/product/generic/system/bin/ot-cli"
-    make clean-ot-cli
-}
-
-main()
-{
-    OPENTHREAD_ENABLE_ANDROID_MK=1 ANDROID_NDK=1 check_datetime
-    OPENTHREAD_ENABLE_ANDROID_MK=1 ANDROID_NDK=1 USE_OTBR_DAEMON=1 check_targets ot-cli ot-ctl
-    OPENTHREAD_ENABLE_ANDROID_MK=1 ANDROID_NDK=1 check_targets ot-cli spi-hdlc-adapter
-}
-
-main "$@"
diff --git a/script/check-api-version b/script/check-api-version
index 1eb58e2..4f3f8bd 100755
--- a/script/check-api-version
+++ b/script/check-api-version
@@ -29,8 +29,11 @@
 
 set -euo pipefail
 
-readonly OT_SHA_OLD="$(git cat-file -p HEAD | grep 'parent ' | head -n1 | cut -d' ' -f2)"
-readonly OT_VERSIONS_FILE=tmp/api_versions
+OT_SHA_OLD="$(git cat-file -p HEAD | grep 'parent ' | head -n1 | cut -d' ' -f2)"
+readonly OT_SHA_OLD
+
+OT_VERSIONS_FILE=tmp/api_versions
+readonly OT_VERSIONS_FILE
 
 die()
 {
diff --git a/script/check-arm-build b/script/check-arm-build
index 2f302d6..398dff7 100755
--- a/script/check-arm-build
+++ b/script/check-arm-build
@@ -29,10 +29,79 @@
 
 set -euxo pipefail
 
+OT_TMP_DIR=/tmp/ot-arm-build-cmake
+readonly OT_TMP_DIR
+
+OT_SHA_NEW=${GITHUB_SHA:-$(git rev-parse HEAD)}
+readonly OT_SHA_NEW
+
+build_nrf52840()
+{
+    local options=(
+        "-DOT_ANYCAST_LOCATOR=ON"
+        "-DOT_BACKBONE_ROUTER=ON"
+        "-DOT_BORDER_AGENT=ON"
+        "-DOT_BORDER_ROUTER=ON"
+        "-DOT_CHANNEL_MANAGER=ON"
+        "-DOT_CHANNEL_MONITOR=ON"
+        "-DOT_COAP=ON"
+        "-DOT_COAPS=ON"
+        "-DOT_COMMISSIONER=ON"
+        "-DOT_CSL_RECEIVER=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_DUA=ON"
+        "-DOT_ECDSA=ON"
+        "-DOT_FULL_LOGS=ON"
+        "-DOT_JAM_DETECTION=ON"
+        "-DOT_JOINER=ON"
+        "-DOT_LINK_METRICS_INITIATOR=ON"
+        "-DOT_LINK_METRICS_SUBJECT=ON"
+        "-DOT_LINK_RAW=ON"
+        "-DOT_MAC_FILTER=ON"
+        "-DOT_MESSAGE_USE_HEAP=ON"
+        "-DOT_MLR=ON"
+        "-DOT_NETDATA_PUBLISHER=ON"
+        "-DOT_NETDIAG_CLIENT=ON"
+        "-DOT_PING_SENDER=ON"
+        "-DOT_SERVICE=ON"
+        "-DOT_SLAAC=ON"
+        "-DOT_SNTP_CLIENT=ON"
+        "-DOT_SRP_CLIENT=ON"
+        "-DOT_SRP_SERVER=ON"
+        "-DOT_THREAD_VERSION=1.3"
+        "-DOT_TIME_SYNC=ON"
+        "-DOT_UDP_FORWARD=ON"
+        "-DOT_UPTIME=ON"
+    )
+
+    rm -rf "${OT_TMP_DIR}"
+
+    script/git-tool clone https://github.com/openthread/ot-nrf528xx.git "${OT_TMP_DIR}"
+    rm -rf "${OT_TMP_DIR}/openthread/*"
+    git archive "${OT_SHA_NEW}" | tar x -C "${OT_TMP_DIR}/openthread"
+
+    cd "${OT_TMP_DIR}"
+    script/build nrf52840 UART_trans "${options[@]}"
+}
+
 main()
 {
-    "$(dirname "$0")"/check-arm-build-autotools
-    "$(dirname "$0")"/check-arm-build-cmake
+    export CPPFLAGS="${CPPFLAGS:-} -DNDEBUG"
+
+    if [[ $# == 0 ]]; then
+        build_nrf52840
+        return 0
+    fi
+
+    while [[ $# != 0 ]]; do
+        "build_$1"
+        shift
+    done
 }
 
 main "$@"
diff --git a/script/check-arm-build-autotools b/script/check-arm-build-autotools
deleted file mode 100755
index e994c20..0000000
--- a/script/check-arm-build-autotools
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/bin/bash
-#
-#  Copyright (c) 2020, 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 -euxo pipefail
-
-reset_source()
-{
-    rm -rf build output tmp
-}
-
-build_cc2538()
-{
-    local options=(
-        "COMMISSIONER=1"
-        "DHCP6_CLIENT=1"
-        "DHCP6_SERVER=1"
-        "DNS_CLIENT=1"
-        "JOINER=1"
-        "SLAAC=1"
-        # cc2538 does not have enough resources to support Thread 1.3
-        "THREAD_VERSION=1.1"
-    )
-
-    reset_source
-    make -f examples/Makefile-cc2538 "${options[@]}"
-}
-
-main()
-{
-    ./bootstrap
-
-    export CPPFLAGS="${CPPFLAGS:-} -DNDEBUG"
-
-    if [[ $# == 0 ]]; then
-        build_cc2538
-        return 0
-    fi
-
-    while [[ $# != 0 ]]; do
-        "build_$1"
-        shift
-    done
-}
-
-main "$@"
diff --git a/script/check-arm-build-cmake b/script/check-arm-build-cmake
deleted file mode 100755
index 3ee62b0..0000000
--- a/script/check-arm-build-cmake
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/bin/bash
-#
-#  Copyright (c) 2020, 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 -euxo pipefail
-
-readonly OT_BUILDDIR="$(pwd)/build"
-
-readonly OT_COMMON_OPTIONS=(
-    "-DOT_COMPILE_WARNING_AS_ERROR=ON"
-)
-
-readonly OT_BASIC_CHECK_OPTIONS=(
-    "-DOT_COMMISSIONER=ON"
-    "-DOT_DHCP6_CLIENT=ON"
-    "-DOT_DHCP6_SERVER=ON"
-    "-DOT_DNS_CLIENT=ON"
-    "-DOT_JOINER=ON"
-)
-
-reset_source()
-{
-    rm -rf "$OT_BUILDDIR"
-}
-
-build_cc2538()
-{
-    local options=(
-        # cc2538 does not have enough resources to support Thread 1.3
-        "-DOT_THREAD_VERSION=1.1"
-    )
-
-    reset_source
-    "$(dirname "$0")"/cmake-build cc2538 "${OT_COMMON_OPTIONS[@]}" "${OT_BASIC_CHECK_OPTIONS[@]}" "${options[@]}"
-}
-
-main()
-{
-    export CPPFLAGS="${CPPFLAGS:-} -DNDEBUG"
-
-    if [[ $# == 0 ]]; then
-        build_cc2538
-        return 0
-    fi
-
-    while [[ $# != 0 ]]; do
-        "build_$1"
-        shift
-    done
-}
-
-main "$@"
diff --git a/script/check-posix-build-cmake b/script/check-posix-build-cmake
index cf9a4f5..a8337bd 100755
--- a/script/check-posix-build-cmake
+++ b/script/check-posix-build-cmake
@@ -29,7 +29,8 @@
 
 set -euxo pipefail
 
-readonly OT_BUILDDIR="$(pwd)/build"
+OT_BUILDDIR="$(pwd)/build"
+readonly OT_BUILDDIR
 
 reset_source()
 {
@@ -50,6 +51,13 @@
         "-DOT_TREL=ON"
     )
 
+    if [[ $OSTYPE != "darwin"* ]]; then
+        options+=(
+            "-DOT_BORDER_ROUTING=ON"
+            "-DOT_SRP_SERVER=ON"
+        )
+    fi
+
     reset_source
     "$(dirname "$0")"/cmake-build posix "${options[@]}"
 }
diff --git a/script/check-posix-pty b/script/check-posix-pty
index 716d154..0cb7939 100755
--- a/script/check-posix-pty
+++ b/script/check-posix-pty
@@ -112,7 +112,8 @@
     $RADIO_NCP_PATH 1 >"$RADIO_PTY" <"$RADIO_PTY" &
 
     # Cover setting a valid network interface name.
-    readonly VALID_NETIF_NAME="wan$(date +%H%M%S)"
+    VALID_NETIF_NAME="wan$(date +%H%M%S)"
+    readonly VALID_NETIF_NAME
 
     RADIO_URL="spinel+hdlc+uart://${CORE_PTY}?region=US&max-power-table=11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26"
 
@@ -140,7 +141,8 @@
         # sleep a while for daemon ready
         sleep 2
 
-        readonly OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH=640
+        OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH=640
+        readonly OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH
         local -r kMaxStringLength="$((OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH - 1))"
 
         # verify success if command length doesn't exceed the limit
@@ -160,7 +162,8 @@
 
         # Cover setting a too long(max is 15 characters) network interface name.
         # Expect exit code to be 2(OT_EXIT_INVALID_ARGUMENTS).
-        readonly INVALID_NETIF_NAME="wan0123456789123"
+        INVALID_NETIF_NAME="wan0123456789123"
+        readonly INVALID_NETIF_NAME
         sudo "${OT_CLI}" -I "${INVALID_NETIF_NAME}" -n "${RADIO_URL}" || test $? = 2
 
         OT_CLI_CMD="$PWD/build/posix/src/posix/ot-cli ${RADIO_URL}"
diff --git a/script/check-scan-build b/script/check-scan-build
index 06329d9..ef723fa 100755
--- a/script/check-scan-build
+++ b/script/check-scan-build
@@ -29,15 +29,16 @@
 
 set -euxo pipefail
 
-readonly OT_SRCDIR="$(pwd)"
+OT_SRCDIR="$(pwd)"
+readonly OT_SRCDIR
 
-readonly OT_BUILD_OPTIONS=(
+OT_BUILD_OPTIONS=(
+    "-DBUILD_TESTING=OFF"
     "-DOT_ANYCAST_LOCATOR=ON"
     "-DOT_BUILD_EXECUTABLES=OFF"
     "-DOT_BORDER_AGENT=ON"
     "-DOT_BORDER_ROUTER=ON"
     "-DOT_BORDER_ROUTING=ON"
-    "-DOT_BORDER_ROUTING_NAT64=ON"
     "-DOT_COAP=ON"
     "-DOT_COAP_BLOCK=ON"
     "-DOT_COAP_OBSERVE=ON"
@@ -47,7 +48,6 @@
     "-DOT_COVERAGE=ON"
     "-DOT_CHANNEL_MANAGER=ON"
     "-DOT_CHANNEL_MONITOR=ON"
-    "-DOT_CHILD_SUPERVISION=ON"
     "-DOT_DATASET_UPDATER=ON"
     "-DOT_DHCP6_CLIENT=ON"
     "-DOT_DHCP6_SERVER=ON"
@@ -59,11 +59,13 @@
     "-DOT_IP6_FRAGM=ON"
     "-DOT_JAM_DETECTION=ON"
     "-DOT_JOINER=ON"
-    "-DOT_LEGACY=ON"
     "-DOT_LOG_LEVEL_DYNAMIC=ON"
     "-DOT_MAC_FILTER=ON"
-    "-DOT_MTD_NETDIAG=ON"
+    "-DOT_MESH_DIAG=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"
@@ -73,22 +75,27 @@
     "-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
 
-readonly OT_CFLAGS=(
+OT_CFLAGS=(
     "-DMBEDTLS_DEBUG_C"
     "-I$(pwd)/third_party/mbedtls"
     "-I$(pwd)/third_party/mbedtls/repo/include"
     '-DMBEDTLS_CONFIG_FILE=\"mbedtls-config.h\"'
 )
+readonly OT_CFLAGS
 
 main()
 {
     mkdir -p build
     cd build
 
-    scan-build-9 cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DOT_COMPILE_WARNING_AS_ERROR=ON -DCMAKE_C_FLAGS="${OT_CFLAGS[*]}" -DCMAKE_CXX_FLAGS="${OT_CFLAGS[*]}" "${OT_BUILD_OPTIONS[@]}" "${OT_SRCDIR}"
-    scan-build-9 --status-bugs -analyze-headers -v ninja
+    scan-build-14 cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DOT_COMPILE_WARNING_AS_ERROR=ON -DCMAKE_C_FLAGS="${OT_CFLAGS[*]}" -DCMAKE_CXX_FLAGS="${OT_CFLAGS[*]}" "${OT_BUILD_OPTIONS[@]}" "${OT_SRCDIR}"
+    scan-build-14 --status-bugs -analyze-headers -v ninja
 
     cd "${OT_SRCDIR}"
 }
diff --git a/script/check-simulation-build-autotools b/script/check-simulation-build-autotools
index aa6f631..1e3ef25 100755
--- a/script/check-simulation-build-autotools
+++ b/script/check-simulation-build-autotools
@@ -29,7 +29,8 @@
 
 set -euxo pipefail
 
-readonly OT_BUILD_JOBS=$(getconf _NPROCESSORS_ONLN)
+OT_BUILD_JOBS=$(getconf _NPROCESSORS_ONLN)
+readonly OT_BUILD_JOBS
 
 reset_source()
 {
@@ -46,7 +47,6 @@
         "-DOPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE=1"
         "-DOPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE=1"
         "-DOPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE=1"
-        "-DOPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE=1"
         "-DOPENTHREAD_CONFIG_COAP_API_ENABLE=1"
         "-DOPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE=1"
         "-DOPENTHREAD_CONFIG_COMMISSIONER_ENABLE=1"
@@ -63,7 +63,6 @@
         "-DOPENTHREAD_CONFIG_IP6_SLAAC_ENABLE=1"
         "-DOPENTHREAD_CONFIG_JAM_DETECTION_ENABLE=1"
         "-DOPENTHREAD_CONFIG_JOINER_ENABLE=1"
-        "-DOPENTHREAD_CONFIG_LEGACY_ENABLE=1"
         "-DOPENTHREAD_CONFIG_LINK_RAW_ENABLE=1"
         "-DOPENTHREAD_CONFIG_LOG_LEVEL_DYNAMIC_ENABLE=1"
         "-DOPENTHREAD_CONFIG_MAC_BEACON_RSP_WHEN_JOINABLE_ENABLE=1"
@@ -87,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"
@@ -95,6 +94,8 @@
 
     local options_1_3=(
         "-DOPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE=1"
+        "-DOPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE=1"
+        "-DOPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE=1"
         "-DOPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE=1"
         "-DOPENTHREAD_CONFIG_DUA_ENABLE=1"
         "-DOPENTHREAD_CONFIG_MLR_ENABLE=1"
@@ -142,11 +143,9 @@
         "-DOPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE=1"
         "-DOPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE=1"
         "-DOPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE=1"
-        "-DOPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE=1"
         "-DOPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE=1"
         "-DOPENTHREAD_CONFIG_DIAG_ENABLE=1"
         "-DOPENTHREAD_CONFIG_JAM_DETECTION_ENABLE=1"
-        "-DOPENTHREAD_CONFIG_LEGACY_ENABLE=1"
         "-DOPENTHREAD_CONFIG_MAC_FILTER_ENABLE=1"
         "-DOPENTHREAD_CONFIG_PING_SENDER_ENABLE=1"
         "-DOPENTHREAD_CONFIG_NCP_SPI_ENABLE=1"
diff --git a/script/check-simulation-build-cmake b/script/check-simulation-build-cmake
index f296ab1..3a0800a 100755
--- a/script/check-simulation-build-cmake
+++ b/script/check-simulation-build-cmake
@@ -29,7 +29,8 @@
 
 set -euxo pipefail
 
-readonly OT_BUILDDIR="$(pwd)/build"
+OT_BUILDDIR="$(pwd)/build"
+readonly OT_BUILDDIR
 
 reset_source()
 {
@@ -57,6 +58,8 @@
     local options=(
         "-DOT_BACKBONE_ROUTER=ON"
         "-DOT_BORDER_ROUTING=ON"
+        "-DOT_NAT64_BORDER_ROUTING=ON"
+        "-DOT_NAT64_TRANSLATOR=ON"
         "-DOT_CSL_RECEIVER=ON"
         "-DOT_MLR=ON"
         "-DOT_OTNS=ON"
diff --git a/script/check-size b/script/check-size
index e81c0bf..9076626 100755
--- a/script/check-size
+++ b/script/check-size
@@ -29,10 +29,17 @@
 
 set -euo pipefail
 
-readonly OT_TMP_DIR=/tmp/ot-size-report
-readonly OT_SHA_NEW=${GITHUB_SHA:-$(git rev-parse HEAD)}
-readonly OT_SHA_OLD="$(git cat-file -p "${OT_SHA_NEW}" | grep 'parent ' | head -n1 | cut -d' ' -f2)"
-readonly OT_REPORT_FILE=/tmp/size_report
+OT_TMP_DIR=/tmp/ot-size-report
+readonly OT_TMP_DIR
+
+OT_SHA_NEW=${GITHUB_SHA:-$(git rev-parse HEAD)}
+readonly OT_SHA_NEW
+
+OT_SHA_OLD="$(git cat-file -p "${OT_SHA_NEW}" | grep 'parent ' | head -n1 | cut -d' ' -f2)"
+readonly OT_SHA_OLD
+
+OT_REPORT_FILE=/tmp/size_report
+readonly OT_REPORT_FILE
 
 setup_arm_gcc_7()
 {
@@ -119,7 +126,6 @@
         "-DOT_BORDER_ROUTER=ON"
         "-DOT_CHANNEL_MANAGER=ON"
         "-DOT_CHANNEL_MONITOR=ON"
-        "-DOT_CHILD_SUPERVISION=ON"
         "-DOT_COAP=ON"
         "-DOT_COAPS=ON"
         "-DOT_COMMISSIONER=ON"
@@ -136,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/clang-format b/script/clang-format
index 01b9535..e31466d 100755
--- a/script/clang-format
+++ b/script/clang-format
@@ -27,7 +27,7 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-CLANG_FORMAT_VERSION="clang-format version 9.0"
+CLANG_FORMAT_VERSION="clang-format version 14.0"
 
 die()
 {
@@ -39,36 +39,41 @@
 # expand_aliases shell option is set using shopt.
 shopt -s expand_aliases
 
-if command -v clang-format-9 >/dev/null; then
-    alias clang-format=clang-format-9
+if command -v clang-format-14 >/dev/null; then
+    alias clang-format=clang-format-14
 elif command -v clang-format >/dev/null; then
     case "$(clang-format --version)" in
         "$CLANG_FORMAT_VERSION"*) ;;
 
         *)
-            die "$(clang-format --version); clang-format 9.0 required"
+            die "$(clang-format --version); clang-format 14.0 required"
             ;;
     esac
 else
-    die "clang-format 9.0 required"
+    die "clang-format 14.0 required"
 fi
 
 clang-format "$@" || die
 
 # ensure EOF newline
 REPLACE=no
+FILES=()
 for arg; do
     case $arg in
         -i)
             REPLACE=yes
             ;;
+        -*) ;;
+        *)
+            FILES+=("$arg")
+            ;;
     esac
 done
 
-file=$arg
-
 [ $REPLACE != yes ] || {
-    [ -n "$(tail -c1 "$file")" ] && echo >>"$file"
+    for file in "${FILES[@]}"; do
+        [ -n "$(tail -c1 "$file")" ] && echo >>"$file"
+    done
 }
 
 exit 0
diff --git a/script/clang-tidy b/script/clang-tidy
index b2dfb4d..c460809 100755
--- a/script/clang-tidy
+++ b/script/clang-tidy
@@ -27,8 +27,8 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-CLANG_TIDY_VERSION="LLVM version 9.0"
-CLANG_APPLY_REPLACEMENTS_VERSION="clang-apply-replacements version 9.0"
+CLANG_TIDY_VERSION="LLVM version 14.0"
+CLANG_APPLY_REPLACEMENTS_VERSION="clang-apply-replacements version 14.0"
 
 die()
 {
@@ -36,9 +36,9 @@
     exit 1
 }
 
-# Search for clang-tidy-9
-if command -v clang-tidy-9 >/dev/null; then
-    clang_tidy=$(command -v clang-tidy-9)
+# Search for clang-tidy-14
+if command -v clang-tidy-14 >/dev/null; then
+    clang_tidy=$(command -v clang-tidy-14)
 elif command -v clang-tidy >/dev/null; then
     clang_tidy=$(command -v clang-tidy)
     case "$($clang_tidy --version)" in
@@ -49,12 +49,12 @@
             ;;
     esac
 else
-    die "clang-tidy 9.0 required"
+    die "clang-tidy 14.0 required"
 fi
 
-# Search for clang-apply-replacements-9
-if command -v clang-apply-replacements-9 >/dev/null; then
-    clang_apply_replacements=$(command -v clang-apply-replacements-9)
+# Search for clang-apply-replacements-14
+if command -v clang-apply-replacements-14 >/dev/null; then
+    clang_apply_replacements=$(command -v clang-apply-replacements-14)
 elif command -v clang-apply-replacements >/dev/null; then
     clang_apply_replacements=$(command -v clang-apply-replacements)
     case "$($clang_apply_replacements --version)" in
@@ -65,20 +65,20 @@
             ;;
     esac
 else
-    die "clang-apply-replacements 9.0 required"
+    die "clang-apply-replacements 14.0 required"
 fi
 
-# Search for run-clang-tidy-9.py
-if command -v run-clang-tidy-9.py >/dev/null; then
-    run_clang_tidy=$(command -v run-clang-tidy-9.py)
-elif command -v run-clang-tidy-9 >/dev/null; then
-    run_clang_tidy=$(command -v run-clang-tidy-9)
+# Search for run-clang-tidy-14.py
+if command -v run-clang-tidy-14.py >/dev/null; then
+    run_clang_tidy=$(command -v run-clang-tidy-14.py)
+elif command -v run-clang-tidy-14 >/dev/null; then
+    run_clang_tidy=$(command -v run-clang-tidy-14)
 elif command -v run-clang-tidy.py >/dev/null; then
     run_clang_tidy=$(command -v run-clang-tidy.py)
 elif command -v run-clang-tidy >/dev/null; then
     run_clang_tidy=$(command -v run-clang-tidy)
 else
-    die "run-clang-tidy.py 9.0 required"
+    die "run-clang-tidy.py 14.0 required"
 fi
 
 $run_clang_tidy -clang-tidy-binary "$clang_tidy" -clang-apply-replacements-binary "$clang_apply_replacements" "$@" || die
diff --git a/script/cmake-build b/script/cmake-build
index b91b7be..4757286 100755
--- a/script/cmake-build
+++ b/script/cmake-build
@@ -45,6 +45,7 @@
 #  Compile with the specified ninja build target:
 #
 #      OT_CMAKE_NINJA_TARGET="ot-cli-ftd" script/cmake-build ${platform}
+#      OT_CMAKE_NINJA_TARGET="ot-cli-ftd ot-cli-mtd" script/cmake-build ${platform}
 #
 #  Compile with the specified build directory:
 #
@@ -61,50 +62,55 @@
 
 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
-readonly OT_PLATFORMS=(cc2538 simulation posix)
-readonly OT_POSIX_SIM_COMMON_OPTIONS=(
+
+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_LEGACY=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
 
 die()
 {
@@ -122,11 +128,11 @@
     cd "${builddir}"
 
     cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DOT_COMPILE_WARNING_AS_ERROR=ON "$@" "${OT_SRCDIR}"
-
-    if [[ -n ${OT_CMAKE_NINJA_TARGET[*]} ]]; then
-        ninja "${OT_CMAKE_NINJA_TARGET[@]}"
-    else
+    if [[ -z ${OT_CMAKE_NINJA_TARGET// /} ]]; then
         ninja
+    else
+        IFS=' ' read -r -a OT_CMAKE_NINJA_TARGET <<<"${OT_CMAKE_NINJA_TARGET}"
+        ninja "${OT_CMAKE_NINJA_TARGET[@]}"
     fi
 
     cd "${OT_SRCDIR}"
@@ -146,30 +152,87 @@
     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"
                 "-DOT_LOG_OUTPUT=PLATFORM_DEFINED"
                 "-DOT_POSIX_MAX_POWER_TABLE=ON"
             )
             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[@]}")
             ;;
-        cc2538)
-            options+=("-DCMAKE_TOOLCHAIN_FILE=examples/platforms/${platform}/arm-none-eabi.cmake" "-DCMAKE_BUILD_TYPE=MinSizeRel")
-            ;;
         *)
             options+=("-DCMAKE_TOOLCHAIN_FILE=examples/platforms/${platform}/arm-none-eabi.cmake")
             ;;
     esac
 
+    options+=(
+        "-DOT_PLATFORM=${platform}"
+    )
     options+=("$@")
     build "${platform}" "${options[@]}"
 }
diff --git a/script/gcda-tool b/script/gcda-tool
index d5bf203..1b757cc 100755
--- a/script/gcda-tool
+++ b/script/gcda-tool
@@ -29,8 +29,11 @@
 
 set -euxo pipefail
 
-readonly OT_MERGED_PROFILES=merged_profiles
-readonly OT_GCOV_PREFIX_BASE=ot-run
+OT_MERGED_PROFILES=merged_profiles
+readonly OT_MERGED_PROFILES
+
+OT_GCOV_PREFIX_BASE=ot-run
+readonly OT_GCOV_PREFIX_BASE
 
 merge_profiles()
 {
diff --git a/script/make-pretty b/script/make-pretty
index 22201c3..28abdf0 100755
--- a/script/make-pretty
+++ b/script/make-pretty
@@ -64,15 +64,25 @@
 
 set -euo pipefail
 
-readonly OT_BUILD_JOBS=$(getconf _NPROCESSORS_ONLN)
-readonly OT_EXCLUDE_DIRS=(third_party doc/site)
+OT_BUILD_JOBS=$(getconf _NPROCESSORS_ONLN)
+readonly OT_BUILD_JOBS
 
-readonly OT_CLANG_SOURCES=('*.c' '*.cc' '*.cpp' '*.h' '*.hpp')
-readonly OT_MARKDOWN_SOURCES=('*.md')
-readonly OT_PYTHON_SOURCES=('*.py')
+OT_EXCLUDE_DIRS=(third_party doc/site)
+readonly OT_EXCLUDE_DIRS
 
-readonly OT_CLANG_TIDY_FIX_DIRS=('examples' 'include' 'src' 'tests')
-readonly OT_CLANG_TIDY_BUILD_OPTS=(
+OT_CLANG_SOURCES=('*.c' '*.cc' '*.cpp' '*.h' '*.hpp')
+readonly OT_CLANG_SOURCES
+
+OT_MARKDOWN_SOURCES=('*.md')
+readonly OT_MARKDOWN_SOURCES
+
+OT_PYTHON_SOURCES=('*.py')
+readonly OT_PYTHON_SOURCES
+
+OT_CLANG_TIDY_FIX_DIRS=('examples' 'include' 'src' 'tests')
+readonly OT_CLANG_TIDY_FIX_DIRS
+
+OT_CLANG_TIDY_BUILD_OPTS=(
     '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON'
     '-DOT_ANYCAST_LOCATOR=ON'
     '-DOT_APP_RCP=OFF'
@@ -83,10 +93,8 @@
     '-DOT_BORDER_AGENT=ON'
     '-DOT_BORDER_ROUTER=ON'
     '-DOT_BORDER_ROUTING=ON'
-    '-DOT_BORDER_ROUTING_NAT64=ON'
     '-DOT_CHANNEL_MANAGER=ON'
     '-DOT_CHANNEL_MONITOR=ON'
-    '-DOT_CHILD_SUPERVISION=ON'
     '-DOT_COAP=ON'
     '-DOT_COAP_BLOCK=ON'
     '-DOT_COAP_OBSERVE=ON'
@@ -99,6 +107,7 @@
     '-DOT_DIAGNOSTIC=ON'
     '-DOT_DNS_CLIENT=ON'
     '-DOT_DNS_DSO=ON'
+    '-DOT_DNS_UPSTREAM_QUERY=ON'
     '-DOT_DNSSD_SERVER=ON'
     '-DOT_DUA=ON'
     '-DOT_MLR=ON'
@@ -107,13 +116,15 @@
     '-DOT_IP6_FRAGM=ON'
     '-DOT_JAM_DETECTION=ON'
     '-DOT_JOINER=ON'
-    '-DOT_LEGACY=ON'
     '-DOT_LINK_RAW=ON'
     '-DOT_LINK_METRICS_INITIATOR=ON'
     '-DOT_LINK_METRICS_SUBJECT=ON'
     '-DOT_MAC_FILTER=ON'
-    '-DOT_MTD_NETDIAG=ON'
+    '-DOT_MESH_DIAG=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'
@@ -128,12 +139,14 @@
     '-DOT_COMPILE_WARNING_AS_ERROR=ON'
     '-DOT_UPTIME=ON'
 )
+readonly OT_CLANG_TIDY_BUILD_OPTS
 
-readonly OT_CLANG_TIDY_CHECKS="\
+OT_CLANG_TIDY_CHECKS="\
 -*,\
 google-explicit-constructor,\
 google-readability-casting,\
 misc-unused-using-decls,\
+modernize-loop-convert,\
 modernize-use-bool-literals,\
 modernize-use-equals-default,\
 modernize-use-equals-delete,\
@@ -142,12 +155,12 @@
 readability-else-after-return,\
 readability-inconsistent-declaration-parameter-name,\
 readability-make-member-function-const,\
+readability-redundant-control-flow,\
 readability-redundant-member-init,\
 readability-simplify-boolean-expr,\
 readability-static-accessed-through-instance,\
 "
-
-#performance-for-range-copy\
+readonly OT_CLANG_TIDY_CHECKS
 
 do_clang_format()
 {
diff --git a/script/package b/script/package
index 4eaafdf..2cd8eef 100755
--- a/script/package
+++ b/script/package
@@ -32,12 +32,14 @@
 
 set -euo pipefail
 
-readonly OT_BUILDDIR="${OT_BUILDDIR:-${PWD}/build}"
+OT_BUILDDIR="${OT_BUILDDIR:-${PWD}/build}"
+readonly OT_BUILDDIR
 
 main()
 {
     local builddir
     local options=(
+        "-DBUILD_TESTING=OFF"
         "-DCMAKE_BUILD_TYPE=Release"
         "-DOT_COVERAGE=OFF"
         "-DOT_LOG_LEVEL=INFO"
diff --git a/script/test b/script/test
index be16f85..7fc0c3e 100755
--- a/script/test
+++ b/script/test
@@ -32,27 +32,62 @@
 
 set -euo pipefail
 
-readonly OT_BUILDDIR="${OT_BUILDDIR:-${PWD}/build}"
-readonly OT_SRCDIR="${PWD}"
+OT_BUILDDIR="${OT_BUILDDIR:-${PWD}/build}"
+readonly OT_BUILDDIR
 
-readonly OT_COLOR_PASS='\033[0;32m'
-readonly OT_COLOR_FAIL='\033[0;31m'
-readonly OT_COLOR_SKIP='\033[0;33m'
-readonly OT_COLOR_NONE='\033[0m'
+OT_SRCDIR="${PWD}"
+readonly OT_SRCDIR
 
-readonly OT_NODE_TYPE="${OT_NODE_TYPE:-cli}"
-readonly OT_NATIVE_IP="${OT_NATIVE_IP:-0}"
-readonly THREAD_VERSION="${THREAD_VERSION:-1.3}"
-readonly INTER_OP="${INTER_OP:-0}"
-readonly VERBOSE="${VERBOSE:-0}"
-readonly BORDER_ROUTING="${BORDER_ROUTING:-1}"
-readonly NAT64="${NAT64:-0}"
-readonly INTER_OP_BBR="${INTER_OP_BBR:-1}"
+OT_COLOR_PASS='\033[0;32m'
+readonly OT_COLOR_PASS
 
-readonly OT_COREDUMP_DIR="${PWD}/ot-core-dump"
-readonly FULL_LOGS=${FULL_LOGS:-0}
-readonly TREL=${TREL:-0}
-readonly LOCAL_OTBR_DIR=${LOCAL_OTBR_DIR:-""}
+OT_COLOR_FAIL='\033[0;31m'
+readonly OT_COLOR_FAIL
+
+OT_COLOR_SKIP='\033[0;33m'
+readonly OT_COLOR_SKIP
+
+OT_COLOR_NONE='\033[0m'
+readonly OT_COLOR_NONE
+
+OT_NODE_TYPE="${OT_NODE_TYPE:-cli}"
+readonly OT_NODE_TYPE
+
+OT_NATIVE_IP="${OT_NATIVE_IP:-0}"
+readonly OT_NATIVE_IP
+
+THREAD_VERSION="${THREAD_VERSION:-1.3}"
+readonly THREAD_VERSION
+
+INTER_OP="${INTER_OP:-0}"
+readonly INTER_OP
+
+VERBOSE="${VERBOSE:-0}"
+readonly VERBOSE
+
+BORDER_ROUTING="${BORDER_ROUTING:-1}"
+readonly BORDER_ROUTING
+
+NAT64="${NAT64:-0}"
+readonly NAT64
+
+NAT64_SERVICE="${NAT64_SERVICE:-openthread}"
+readonly NAT64_SERVICE
+
+INTER_OP_BBR="${INTER_OP_BBR:-0}"
+readonly INTER_OP_BBR
+
+OT_COREDUMP_DIR="${PWD}/ot-core-dump"
+readonly OT_COREDUMP_DIR
+
+FULL_LOGS=${FULL_LOGS:-0}
+readonly FULL_LOGS
+
+TREL=${TREL:-0}
+readonly TREL
+
+LOCAL_OTBR_DIR=${LOCAL_OTBR_DIR:-""}
+readonly LOCAL_OTBR_DIR
 
 build_simulation()
 {
@@ -237,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"
@@ -250,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
 }
 
@@ -286,9 +323,27 @@
         "-DOT_SRP_CLIENT=ON"
         "-DOT_FULL_LOGS=ON"
         "-DOT_UPTIME=ON"
+        "-DOTBR_DNS_UPSTREAM_QUERY=ON"
         "-DOTBR_DUA_ROUTING=ON"
-        "-DCMAKE_CXX_FLAGS='-DOPENTHREAD_CONFIG_DNSSD_SERVER_BIND_UNSPECIFIED_NETIF=1'"
     )
+    local args=(
+        "BORDER_ROUTING=${BORDER_ROUTING}"
+        "INFRA_IF_NAME=eth0"
+        "BACKBONE_ROUTER=1"
+        "REFERENCE_DEVICE=1"
+        "OT_BACKBONE_CI=1"
+        "NAT64=${NAT64}"
+        "NAT64_SERVICE=${NAT64_SERVICE}"
+        "DNS64=${NAT64}"
+        "REST_API=0"
+        "WEB_GUI=0"
+        "MDNS=${OTBR_MDNS:-mDNSResponder}"
+    )
+
+    if [[ ${NAT64} != 1 ]]; then
+        # We are testing upstream DNS forwarding in the NAT64 tests, and OPENTHREAD_CONFIG_DNSSD_SERVER_BIND_UNSPECIFIED_NETIF will block OpenThread's DNSSD server since we already have bind9 running.
+        otbr_options+=("-DCMAKE_CXX_FLAGS='-DOPENTHREAD_CONFIG_DNSSD_SERVER_BIND_UNSPECIFIED_NETIF=1'")
+    fi
 
     if [[ ${TREL} == 1 ]]; then
         otbr_options+=("-DOTBR_TREL=ON")
@@ -296,13 +351,12 @@
         otbr_options+=("-DOTBR_TREL=OFF")
     fi
 
-    if [[ ${NAT64} == 1 ]]; then
-        otbr_options+=("-DOTBR_BORDER_ROUTING_NAT64=ON")
-    else
-        otbr_options+=("-DOTBR_BORDER_ROUTING_NAT64=OFF")
-    fi
-
     local otbr_docker_image=${OTBR_DOCKER_IMAGE:-otbr-ot12-backbone-ci}
+    local docker_build_args=()
+
+    for arg in "${args[@]}"; do
+        docker_build_args+=("--build-arg" "$arg")
+    done
 
     otbrdir=$(mktemp -d -t otbr_XXXXXX)
     otdir=$(pwd)
@@ -311,23 +365,24 @@
         if [[ -z ${LOCAL_OTBR_DIR} ]]; then
             ./script/git-tool clone https://github.com/openthread/ot-br-posix.git --depth 1 "${otbrdir}"
         else
-            cp -r "${LOCAL_OTBR_DIR}"/* "${otbrdir}"
-            rm -rf "${otbrdir}"/build
+            rsync -r \
+                --exclude=third_party/openthread/repo \
+                --exclude=.git \
+                --exclude=build \
+                "${LOCAL_OTBR_DIR}/." \
+                "${otbrdir}"
         fi
+
         cd "${otbrdir}"
         rm -rf third_party/openthread/repo
-        cp -r "${otdir}" third_party/openthread/repo
+        rsync -r \
+            --exclude=build \
+            "${otdir}/." \
+            third_party/openthread/repo
         rm -rf .git
+
         docker build -t "${otbr_docker_image}" -f etc/docker/Dockerfile . \
-            --build-arg BORDER_ROUTING="${BORDER_ROUTING}" \
-            --build-arg INFRA_IF_NAME=eth0 \
-            --build-arg BACKBONE_ROUTER=1 \
-            --build-arg REFERENCE_DEVICE=1 \
-            --build-arg OT_BACKBONE_CI=1 \
-            --build-arg NAT64="${NAT64}" \
-            --build-arg REST_API=0 \
-            --build-arg WEB_GUI=0 \
-            --build-arg MDNS="${OTBR_MDNS:-mDNSResponder}" \
+            "${docker_build_args[@]}" \
             --build-arg OTBR_OPTIONS="${otbr_options[*]}"
     )
 
@@ -336,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 2cd45d3..bbaeca4 100755
--- a/script/update-makefiles.py
+++ b/script/update-makefiles.py
@@ -148,18 +148,6 @@
 print("Updated " + include_build_gn_file)
 
 #----------------------------------------------------------------------------------------------
-# Update Android.mk file
-
-android_mk_file = "./Android.mk"
-
-formatted_list = ["    {:<63} \\\n".format(file_name) for file_name in core_cpp_files]
-start_string = "LOCAL_SRC_FILES                                                  := \\\n"
-end_string = "    src/lib/hdlc/hdlc.cpp"
-update_build_file(android_mk_file, start_string, end_string, formatted_list)
-
-print("Updated " + android_mk_file)
-
-#----------------------------------------------------------------------------------------------
 # Update Makefile.am files
 
 core_makefile_am_file = "./src/core/Makefile.am"
diff --git a/src/android/config-android-version-gen.sh b/src/android/config-android-version-gen.sh
index 874ff2c..ee88f61 100755
--- a/src/android/config-android-version-gen.sh
+++ b/src/android/config-android-version-gen.sh
@@ -32,23 +32,5 @@
 
 set -uo pipefail
 
-main()
-{
-    # "Usage: config-android-version-gen.sh < src/android/openthread-config-android-version.h.in > openthread-config-android-version.h"
-
-    OPENTHREAD_VERSION_GEN_FILE="$0"
-    OPENTHREAD_VERSION_GEN_FILE_PATH=$(dirname "${OPENTHREAD_VERSION_GEN_FILE}")
-
-    cd ${OPENTHREAD_VERSION_GEN_FILE_PATH}
-    INSIDE_GIT_REPO=$(git rev-parse --is-inside-work-tree 2>/dev/null)
-
-    if [ "${INSIDE_GIT_REPO}" == "true" ]; then
-        OPENTHREAD_SOURCE_VERSION=$(git describe --dirty --always)
-    else
-        OPENTHREAD_SOURCE_VERSION="Unknown"
-    fi
-
-    sed -e s/@OPENTHREAD_SOURCE_VERSION@/"${OPENTHREAD_SOURCE_VERSION}"/
-}
-
-main "$@"
+# "Usage: config-android-version-gen.sh < src/android/openthread-config-android-version.h.in > openthread-config-android-version.h"
+sed -e s/@OPENTHREAD_SOURCE_VERSION@/Unknown/ "$@"
diff --git a/src/android/openthread-android-config.h b/src/android/openthread-android-config.h
index f6dde3b..766b4ef 100644
--- a/src/android/openthread-android-config.h
+++ b/src/android/openthread-android-config.h
@@ -43,3 +43,22 @@
  *
  */
 #define OPENTHREAD_CONFIG_CLI_UART_RX_BUFFER_SIZE 3500
+
+/**
+ * Disables the default posix infrastructure interface implementation
+ * so that we can can use the Android specific implementation.
+ */
+#define OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE 0
+
+/**
+ * Disables the default posix TUN interface implementation
+ * so that we can can use the Android specific implementation.
+ */
+#define OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE 0
+
+/**
+ * Temporarily disable PLATFORM_UDP to make ot-daemon usable with the command line "ot-ctl" tool.
+ */
+// FIXME(296975198): refactor to skip posix/udp.cpp when the tunnel interface is not
+// available, instead of crash
+#define OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE 0
diff --git a/src/android/openthread-core-android-config.h b/src/android/openthread-core-android-config.h
index a91da51..cfe6e11 100644
--- a/src/android/openthread-core-android-config.h
+++ b/src/android/openthread-core-android-config.h
@@ -275,15 +275,19 @@
 
 #define OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE 1
 #define OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE 1
+#define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 1
+#define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1
+#define OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE 1
 #define OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE 1
 #define OPENTHREAD_CONFIG_ECDSA_ENABLE 1
 #define OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE 1
 #define OPENTHREAD_CONFIG_LOG_LEVEL_DYNAMIC_ENABLE 1
 #define OPENTHREAD_CONFIG_MAC_FILTER_ENABLE 1
 #define OPENTHREAD_CONFIG_PING_SENDER_ENABLE 1
-#define OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE 1
-#define OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE 1
 #define OPENTHREAD_CONFIG_SRP_SERVER_ENABLE 1
 #define OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE 1
 
+// Disables built-in TCP support as TCP can be support on upper layer
+#define OPENTHREAD_CONFIG_TCP_ENABLE 0
+
 #endif // OPENTHREAD_CORE_ANDROID_CONFIG_H_
diff --git a/src/android/ot-cli-ftd.rc b/src/android/ot-cli-ftd.rc
new file mode 100644
index 0000000..57d2d29
--- /dev/null
+++ b/src/android/ot-cli-ftd.rc
@@ -0,0 +1,4 @@
+# The settings data directory for simulation `ot-cli-ftd` program
+on post-fs-data
+    mkdir /data/vendor/threadnetwork 0770 root root
+    mkdir /data/vendor/threadnetwork/simulation 0770 root root
diff --git a/src/android/thread_network_hal/device_manifest.xml b/src/android/thread_network_hal/device_manifest.xml
new file mode 100644
index 0000000..d7dee1e
--- /dev/null
+++ b/src/android/thread_network_hal/device_manifest.xml
@@ -0,0 +1,6 @@
+<manifest version="1.0" type="device">
+    <hal format="aidl">
+        <name>android.hardware.threadnetwork</name>
+        <fqname>IThreadChip/chip0</fqname>
+    </hal>
+</manifest>
diff --git a/src/android/thread_network_hal/hal_interface.cpp b/src/android/thread_network_hal/hal_interface.cpp
index 25d6292..88545b5 100644
--- a/src/android/thread_network_hal/hal_interface.cpp
+++ b/src/android/thread_network_hal/hal_interface.cpp
@@ -145,26 +145,32 @@
     return kBusSpeed;
 }
 
-void HalInterface::OnRcpReset(void)
+otError HalInterface::HardwareReset(void)
 {
-    mThreadChip->reset();
+    mThreadChip->hardwareReset();
+    return OT_ERROR_NONE;
 }
 
-void HalInterface::UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, int &aMaxFd, struct timeval &aTimeout)
+void HalInterface::UpdateFdSet(void *aMainloopContext)
 {
-    OT_UNUSED_VARIABLE(aWriteFdSet);
-    OT_UNUSED_VARIABLE(aTimeout);
+    otSysMainloopContext *context = reinterpret_cast<otSysMainloopContext *>(aMainloopContext);
+
+    assert(context != nullptr);
 
     if (mBinderFd >= 0)
     {
-        FD_SET(mBinderFd, &aReadFdSet);
-        aMaxFd = std::max(aMaxFd, mBinderFd);
+        FD_SET(mBinderFd, &context->mReadFdSet);
+        context->mMaxFd = std::max(context->mMaxFd, mBinderFd);
     }
 }
 
-void HalInterface::Process(const RadioProcessContext &aContext)
+void HalInterface::Process(const void *aMainloopContext)
 {
-    if ((mBinderFd >= 0) && FD_ISSET(mBinderFd, aContext.mReadFdSet))
+    const otSysMainloopContext *context = reinterpret_cast<const otSysMainloopContext *>(aMainloopContext);
+
+    assert(context != nullptr);
+
+    if ((mBinderFd >= 0) && FD_ISSET(mBinderFd, &context->mReadFdSet))
     {
         ABinderProcess_handlePolledCommands();
     }
@@ -261,6 +267,10 @@
     {
         error = OT_ERROR_INVALID_ARGS;
     }
+    else if (aStatus.getExceptionCode() == EX_UNSUPPORTED_OPERATION)
+    {
+        error = OT_ERROR_NOT_IMPLEMENTED;
+    }
     else if (aStatus.getExceptionCode() == EX_SERVICE_SPECIFIC)
     {
         switch (aStatus.getServiceSpecificError())
diff --git a/src/android/thread_network_hal/hal_interface.hpp b/src/android/thread_network_hal/hal_interface.hpp
index da19390..667dd47 100644
--- a/src/android/thread_network_hal/hal_interface.hpp
+++ b/src/android/thread_network_hal/hal_interface.hpp
@@ -123,21 +123,18 @@
     /**
      * This method updates the file descriptor sets with file descriptors used by the radio driver.
      *
-     * @param[inout]  aReadFdSet   A reference to the read file descriptors.
-     * @param[inout]  aWriteFdSet  A reference to the write file descriptors.
-     * @param[inout]  aMaxFd       A reference to the max file descriptor.
-     * @param[inout]  aTimeout     A reference to the timeout.
+     * @param[in]   aMainloopContext  The context containing fd_sets.
      *
      */
-    void UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, int &aMaxFd, struct timeval &aTimeout);
+    void UpdateFdSet(void *aMainloopContext);
 
     /**
      * This method performs radio driver processing.
      *
-     * @param[in]   aContext        The context containing fd_sets.
+     * @param[in]   aMainloopContext  The context containing fd_sets.
      *
      */
-    void Process(const RadioProcessContext &aContext);
+    void Process(const void *aMainloopContext);
 
     /**
      * This method returns the bus speed between the host and the radio.
@@ -148,17 +145,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. It will be called after a software reset fails.
+     *
+     * @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);
 
 private:
     void        ReceiveFrameCallback(const std::vector<uint8_t> &aFrame);
diff --git a/src/android/thread_network_hal/vendor_interface.cpp b/src/android/thread_network_hal/vendor_interface.cpp
index fb8c66f..69fbe0a 100644
--- a/src/android/thread_network_hal/vendor_interface.cpp
+++ b/src/android/thread_network_hal/vendor_interface.cpp
@@ -81,19 +81,14 @@
     return sHalInterface->GetBusSpeed();
 }
 
-void VendorInterface::OnRcpReset(void)
+void VendorInterface::UpdateFdSet(void *aMainloopContext)
 {
-    sHalInterface->OnRcpReset();
+    sHalInterface->UpdateFdSet(aMainloopContext);
 }
 
-void VendorInterface::UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, int &aMaxFd, struct timeval &aTimeout)
+void VendorInterface::Process(const void *aMainloopContext)
 {
-    sHalInterface->UpdateFdSet(aReadFdSet, aWriteFdSet, aMaxFd, aTimeout);
-}
-
-void VendorInterface::Process(const RadioProcessContext &aContext)
-{
-    sHalInterface->Process(aContext);
+    sHalInterface->Process(aMainloopContext);
 }
 
 otError VendorInterface::WaitForFrame(uint64_t aTimeoutUs)
@@ -106,9 +101,14 @@
     return sHalInterface->SendFrame(aFrame, aLength);
 }
 
-otError VendorInterface::ResetConnection(void)
+otError VendorInterface::HardwareReset(void)
 {
-    return sHalInterface->ResetConnection();
+    return sHalInterface->HardwareReset();
+}
+
+const otRcpInterfaceMetrics *VendorInterface::GetRcpInterfaceMetrics(void) const
+{
+    return nullptr;
 }
 } // namespace Posix
 } // namespace ot
diff --git a/src/cli/BUILD.gn b/src/cli/BUILD.gn
index 2d4e8f6..86e06af 100644
--- a/src/cli/BUILD.gn
+++ b/src/cli/BUILD.gn
@@ -30,6 +30,8 @@
 openthread_cli_sources = [
   "cli.cpp",
   "cli.hpp",
+  "cli_br.cpp",
+  "cli_br.hpp",
   "cli_coap.cpp",
   "cli_coap.hpp",
   "cli_coap_secure.cpp",
diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt
index 67c8b01..32c9d7b 100644
--- a/src/cli/CMakeLists.txt
+++ b/src/cli/CMakeLists.txt
@@ -33,6 +33,7 @@
 
 set(COMMON_SOURCES
     cli.cpp
+    cli_br.cpp
     cli_coap.cpp
     cli_coap_secure.cpp
     cli_commissioner.cpp
diff --git a/src/cli/Makefile.am b/src/cli/Makefile.am
index f6e32d0..6df2c08 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)
@@ -153,6 +153,7 @@
 
 SOURCES_COMMON =                      \
     cli.cpp                           \
+    cli_br.cpp                        \
     cli_coap.cpp                      \
     cli_coap_secure.cpp               \
     cli_commissioner.cpp              \
@@ -182,6 +183,7 @@
 
 noinst_HEADERS                      = \
     cli.hpp                           \
+    cli_br.hpp                        \
     cli_coap.hpp                      \
     cli_coap_secure.hpp               \
     cli_commissioner.hpp              \
diff --git a/src/cli/README.md b/src/cli/README.md
index c088da4..bdc405b 100644
--- a/src/cli/README.md
+++ b/src/cli/README.md
@@ -23,7 +23,7 @@
 
 - [ba](#ba)
 - [bbr](#bbr)
-- [br](#br)
+- [br](README_BR.md)
 - [bufferinfo](#bufferinfo)
 - [ccathreshold](#ccathreshold)
 - [channel](#channel)
@@ -34,12 +34,14 @@
 - [childtimeout](#childtimeout)
 - [coap](README_COAP.md)
 - [coaps](README_COAPS.md)
+- [coex](#coex)
 - [commissioner](README_COMMISSIONER.md)
 - [contextreusedelay](#contextreusedelay)
 - [counters](#counters)
 - [csl](#csl)
 - [dataset](README_DATASET.md)
 - [delaytimermin](#delaytimermin)
+- [deviceprops](#deviceprops)
 - [diag](#diag)
 - [discover](#discover-channel)
 - [dns](#dns-config)
@@ -66,10 +68,12 @@
 - [log](#log-filename-filename)
 - [mac](#mac-retries-direct)
 - [macfilter](#macfilter)
+- [meshdiag](#meshdiag-topology)
 - [mliid](#mliid-iid)
 - [mlr](#mlr-reg-ipaddr--timeout)
 - [mode](#mode)
 - [multiradio](#multiradio)
+- [nat64](#nat64-cidr)
 - [neighbor](#neighbor-list)
 - [netdata](README_NETDATA.md)
 - [netstat](#netstat)
@@ -82,12 +86,14 @@
 - [parent](#parent)
 - [parentpriority](#parentpriority)
 - [partitionid](#partitionid)
-- [ping](#ping--i-source-ipaddr-size-count-interval-hoplimit-timeout)
+- [ping](#ping-async--i-source-ipaddr-size-count-interval-hoplimit-timeout)
+- [platform](#platform)
 - [pollperiod](#pollperiod-pollperiod)
 - [preferrouterid](#preferrouterid-routerid)
 - [prefix](#prefix)
 - [promiscuous](#promiscuous)
-- [pskc](#pskc--p-keypassphrase)
+- [pskc](#pskc)
+- [pskcref](#pskcref)
 - [radiofilter](#radiofilter)
 - [rcp](#rcp)
 - [region](#region)
@@ -100,6 +106,7 @@
 - [routereligible](#routereligible)
 - [routerselectionjitter](#routerselectionjitter)
 - [routerupgradethreshold](#routerupgradethreshold)
+- [childrouterlinks](#childrouterlinks)
 - [scan](#scan-channel)
 - [service](#service)
 - [singleton](#singleton)
@@ -114,6 +121,7 @@
 - [udp](README_UDP.md)
 - [unsecureport](#unsecureport-add-port)
 - [uptime](#uptime)
+- [vendor](#vendor-name)
 - [version](#version)
 
 ## OpenThread Command Details
@@ -247,7 +255,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.
 
@@ -258,7 +266,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
@@ -267,7 +275,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.
 
@@ -348,77 +356,13 @@
 Done
 ```
 
-### br
-
-Enbale/disable the Border Routing functionality.
-
-```bash
-> br enable
-Done
-```
-
-```bash
-> br disable
-Done
-```
-
-### br omrprefix
-
-Get the randomly generated off-mesh-routable prefix of the Border Router.
-
-```bash
-> br omrprefix
-fdfc:1ff5:1512:5622::/64
-Done
-```
-
-### br onlinkprefix
-
-Get the randomly generated on-link prefix of the Border Router.
-
-```bash
-> br onlinkprefix
-fd41:2650:a6f5:0::/64
-Done
-```
-
-### br nat64prefix
-
-Get the local NAT64 prefix of the Border Router.
-
-`OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE` is required.
-
-```bash
-> br nat64prefix
-fd14:1078:b3d5:b0b0:0:0::/96
-Done
-```
-
-### br rioprf
-
-Get the preference used when advertising Route Info Options (e.g., for discovered OMR prefixes) in emitted Router Advertisement message.
-
-```bash
-> br rioprf
-med
-Done
-```
-
-### br rioprf \<prf\>
-
-Set the preference (which may be 'high', 'med', or 'low') to use when advertising Route Info Options (e.g., for discovered OMR prefixes) in emitted Router Advertisement message.
-
-```bash
-> br rioprf low
-Done
-```
-
 ### bufferinfo
 
 Show the current message buffer information.
 
 - The `total` shows total number of message buffers in pool.
 - The `free` shows the number of free message buffers.
+- The `max-used` shows the maximum number of used buffers at the same time since OT stack initialization or last `bufferinfo reset`.
 - This is then followed by info about different queues used by OpenThread stack, each line representing info about a queue.
   - The first number shows number messages in the queue.
   - The second number shows number of buffers used by all messages in the queue.
@@ -428,6 +372,7 @@
 > bufferinfo
 total: 40
 free: 40
+max-used: 5
 6lo send: 0 0 0
 6lo reas: 0 0 0
 ip6: 0 0 0
@@ -439,6 +384,15 @@
 Done
 ```
 
+### bufferinfo reset
+
+Reset the message buffer counter tracking maximum number buffers in use at the same time.
+
+```bash
+> bufferinfo reset
+Done
+```
+
 ### ccathreshold
 
 Get the CCA threshold in dBm measured at antenna connector per IEEE 802.15.4 - 2015 section 10.1.4.
@@ -672,10 +626,10 @@
 
 ```bash
 > child table
-| ID  | RLOC16 | Timeout    | Age        | LQ In | C_VN |R|D|N|Ver|CSL|QMsgCnt| Extended MAC     |
-+-----+--------+------------+------------+-------+------+-+-+-+---+---+-------+------------------+
-|   1 | 0xc801 |        240 |         24 |     3 |  131 |1|0|0|  3| 0 |     0 | 4ecede68435358ac |
-|   2 | 0xc802 |        240 |          2 |     3 |  131 |0|0|0|  3| 1 |     0 | a672a601d2ce37d8 |
+| ID  | RLOC16 | Timeout    | Age        | LQ In | C_VN |R|D|N|Ver|CSL|QMsgCnt|Suprvsn| Extended MAC     |
++-----+--------+------------+------------+-------+------+-+-+-+---+---+-------+-------+------------------+
+|   1 | 0xc801 |        240 |         24 |     3 |  131 |1|0|0|  3| 0 |     0 |   129 | 4ecede68435358ac |
+|   2 | 0xc802 |        240 |          2 |     3 |  131 |0|0|0|  3| 1 |     0 |     0 | a672a601d2ce37d8 |
 Done
 ```
 
@@ -689,11 +643,13 @@
 Rloc: 9c01
 Ext Addr: e2b3540590b0fd87
 Mode: rn
+CSL Synchronized: 1
 Net Data: 184
 Timeout: 100
 Age: 0
 Link Quality In: 3
 RSSI: -20
+Supervision Interval: 129
 Done
 ```
 
@@ -789,6 +745,27 @@
 Done
 ```
 
+### childsupervision failcounter
+
+Get the current value of supervision check timeout failure counter.
+
+The counter tracks the number of supervision check failures on the child. It is incremented when the child does not hear from its parent within the specified check timeout interval.
+
+```bash
+> childsupervision failcounter
+0
+Done
+```
+
+### childsupervision failcounter reset
+
+Reset the supervision check timeout failure counter to zero.
+
+```bash
+> childsupervision failcounter reset
+Done
+```
+
 ### childtimeout
 
 Get the Thread Child Timeout value.
@@ -808,6 +785,72 @@
 Done
 ```
 
+### coex
+
+Get the coex status.
+
+`OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE` is required.
+
+```bash
+> coex
+Enabled
+Done
+```
+
+### coex disable
+
+Disable coex.
+
+`OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE` is required.
+
+```bash
+> coex disable
+Done
+```
+
+### coex enable
+
+Enable coex.
+
+`OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE` is required.
+
+```bash
+> coex enable
+Done
+```
+
+### coex metrics
+
+Show coex metrics.
+
+`OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE` is required.
+
+```bash
+> coex metrics
+Stopped: false
+Grant Glitch: 0
+Transmit metrics
+    Request: 0
+    Grant Immediate: 0
+    Grant Wait: 0
+    Grant Wait Activated: 0
+    Grant Wait Timeout: 0
+    Grant Deactivated During Request: 0
+    Delayed Grant: 0
+    Average Request To Grant Time: 0
+Receive metrics
+    Request: 0
+    Grant Immediate: 0
+    Grant Wait: 0
+    Grant Wait Activated: 0
+    Grant Wait Timeout: 0
+    Grant Deactivated During Request: 0
+    Delayed Grant: 0
+    Average Request To Grant Time: 0
+    Grant None: 0
+Done
+```
+
 ### contextreusedelay
 
 Get the CONTEXT_ID_REUSE_DELAY value.
@@ -833,6 +876,7 @@
 
 ```bash
 > counters
+br
 ip
 mac
 mle
@@ -843,6 +887,11 @@
 
 Get the counter value.
 
+Note:
+
+- `OPENTHREAD_CONFIG_UPTIME_ENABLE` is required for MLE role time tracking in `counters mle`
+- `OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE` is required for `counters br`
+
 ```bash
 > counters mac
 TxTotal: 10
@@ -887,6 +936,12 @@
 Partition Id Changes: 1
 Better Partition Attach Attempts: 0
 Parent Changes: 0
+Time Disabled Milli: 10026
+Time Detached Milli: 6852
+Time Child Milli: 0
+Time Router Milli: 0
+Time Leader Milli: 16195
+Time Tracked Milli: 33073
 Done
 > counters ip
 TxSuccess: 10
@@ -894,6 +949,18 @@
 RxSuccess: 5
 RxFailed: 0
 Done
+> counters br
+Inbound Unicast: Packets 4 Bytes 320
+Inbound Multicast: Packets 0 Bytes 0
+Outbound Unicast: Packets 2 Bytes 160
+Outbound Multicast: Packets 0 Bytes 0
+RA Rx: 4
+RA TxSuccess: 2
+RA TxFailed: 0
+RS Rx: 0
+RS TxSuccess: 2
+RS TxFailed: 0
+Done
 ```
 
 ### counters \<countername\> reset
@@ -991,6 +1058,44 @@
 Done
 ```
 
+### deviceprops
+
+Get the current device properties.
+
+```bash
+> deviceprops
+PowerSupply      : external
+IsBorderRouter   : yes
+SupportsCcm      : no
+IsUnstable       : no
+WeightAdjustment : 0
+Done
+```
+
+### deviceprops \<power-supply\> \<is-br\> \<supports-ccm\> \<is-unstable\> \<weight-adjustment\>
+
+Set the device properties which are then used to determine and set the Leader Weight.
+
+- power-supply: `battery`, `external`, `external-stable`, or `external-unstable`.
+- weight-adjustment: Valid range is from -16 to +16. Clamped if not within the range.
+
+```bash
+> deviceprops battery 0 0 0 -5
+Done
+
+> deviceprops
+PowerSupply      : battery
+IsBorderRouter   : no
+SupportsCcm      : no
+IsUnstable       : no
+WeightAdjustment : -5
+Done
+
+> leaderweight
+51
+Done
+```
+
 ### discover \[channel\]
 
 Perform an MLE Discovery operation.
@@ -1009,7 +1114,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
@@ -1017,16 +1135,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)\]
+### 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
+> dns config fd00::1 1234 5000 2 0 srv_txt_sep tcp
 Done
 
 > dns config
@@ -1034,6 +1166,9 @@
 ResponseTimeout: 5000 ms
 MaxTxAttempts: 2
 RecursionDesired: no
+ServiceMode: srv_txt_sep
+Nat64Mode: allow
+TransportProtocol: tcp
 Done
 ```
 
@@ -1051,17 +1186,47 @@
 Done
 ```
 
-### dns resolve \<hostname\> \[DNS server IP\] \[DNS server port\] \[response timeout (ms)\] \[max tx attempts\] \[recursion desired (boolean)\]
+### dns resolve \<hostname\> \[DNS server IP\] \[DNS server port\] \[response timeout (ms)\] \[max tx attempts\] \[recursion desired (boolean)\] \[transport protocol\]
 
 Send DNS Query to obtain IPv6 address for given hostname.
 
 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`).
 
+To use tcp, `OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE` is required.
+
 ```bash
 > dns resolve ipv6.google.com
 > DNS response for ipv6.google.com - 2a00:1450:401b:801:0:0:0:200e TTL: 300
 ```
 
+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.
+
+> Note: The command will return `InvalidState` when the DNS server IP is an IPv4 address but the preferred NAT64 prefix is unavailable.
+
+```bash
+> 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
+```
+
+### dns resolve4 \<hostname\> \[DNS server IP\] \[DNS server port\] \[response timeout (ms)\] \[max tx attempts\] \[recursion desired (boolean)\]
+
+Send DNS query to obtain IPv4 address for a given hostname and provide the NAT64 synthesized IPv6 addresses for the IPv4 addresses from the query response.
+
+Requires `OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE`.
+
+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`).
+
+This command requires a NAT64 prefix to be configured and present in Thread Network Data.
+
+For example, if a NAT64 prefix of `2001:db8:122:344::/96` is used within the Thread mesh, the outputted IPv6 address corresponds to an IPv4 address of `142.250.191.78` for the `ipv4.google.com` host:
+
+```bash
+> dns resolve4 ipv4.google.com
+> DNS response for ipv4.google.com - 2001:db8:122:344:0:0:8efa:bf4e TTL: 20456
+```
+
 ### dns browse \<service-name\> \[DNS server IP\] \[DNS server port\] \[response timeout (ms)\] \[max tx attempts\] \[recursion desired (boolean)\]
 
 Send a browse (service instance enumeration) DNS query to get the list of services for given service-name.
@@ -1084,12 +1249,26 @@
 Done
 ```
 
+```bash
+> dns browse _airplay._tcp.default.service.arpa
+DNS browse response for _airplay._tcp.default.service.arpa.
+Gabe's Mac mini
+    Port:7000, Priority:0, Weight:0, TTL:10
+    Host:Gabes-Mac-mini.default.service.arpa.
+    HostAddress:fd97:739d:386a:1:1c2e:d83c:fcbe:9cf4 TTL:10
+Done
+```
+
+> 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`.
+
 ### dns service \<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. 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`).
 
+> 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 compression \[enable|disable\]
 
 Enable/Disable the "DNS name compression" mode.
@@ -1725,6 +1904,141 @@
 Done
 ```
 
+### meshdiag topology [ip6-addrs][children]
+
+Discover network topology (list of routers and their connections).
+
+This command requires `OPENTHREAD_CONFIG_MESH_DIAG_ENABLE` and `OPENTHREAD_FTD`.
+
+Parameters are optional and indicate additional items to discover. Can be added in any order.
+
+- `ip6-addrs` to discover the list of IPv6 addresses of every router.
+- `children` to discover the child table of every router.
+
+Output lists all discovered routers. Information per router:
+
+- Router ID
+- RLOC16
+- Extended MAC address
+- Thread Version (if known).
+- Whether the router is this device is itself (`me`)
+- Whether the router is the parent of this device when device is a child (`parent`)
+- Whether the router is `leader`
+- Whether the router acts as a border router providing external connectivity (`br`)
+- List of routers to which this router has a link:
+  - `3-links`: Router IDs to which this router has a incoming link with link quality 3
+  - `2-links`: Router IDs to which this router has a incoming link with link quality 2
+  - `1-links`: Router IDs to which this router has a incoming link with link quality 1
+  - If a list if empty, it is omitted in the out.
+- If `ip6-addrs`, list of IPv6 addresses of the router
+- If `children`, list of all children of the router. Information per child:
+  - RLOC16
+  - Incoming Link Quality from perspective of parent to child (zero indicates unknown)
+  - Child Device mode (`r` rx-on-when-idle, `d` Full Thread Device, `n` Full Network Data, `-` no flags set)
+  - Whether the child is this device itself (`me`)
+  - Whether the child acts as a border router providing external connectivity (`br`)
+
+Discover network topology:
+
+```bash
+> meshdiag topology
+id:02 rloc16:0x0800 ext-addr:8aa57d2c603fe16c ver:4 - me - leader
+   3-links:{ 46 }
+id:46 rloc16:0xb800 ext-addr:fe109d277e0175cc ver:4
+   3-links:{ 02 51 57 }
+id:33 rloc16:0x8400 ext-addr:d2e511a146b9e54d ver:4
+   3-links:{ 51 57 }
+id:51 rloc16:0xcc00 ext-addr:9aab43ababf05352 ver:4
+   3-links:{ 33 57 }
+   2-links:{ 46 }
+id:57 rloc16:0xe400 ext-addr:dae9c4c0e9da55ff ver:4
+   3-links:{ 46 51 }
+   1-links:{ 33 }
+Done
+```
+
+Discover network topology with router's IPv6 addresses and children:
+
+```bash
+> meshdiag topology children ip6-addrs
+id:62 rloc16:0xf800 ext-addr:ce349873897233a5 ver:4 - me - br
+   3-links:{ 46 }
+   ip6-addrs:
+       fdde:ad00:beef:0:0:ff:fe00:f800
+       fdde:ad00:beef:0:211d:39e9:6b2e:4ad1
+       fe80:0:0:0:cc34:9873:8972:33a5
+   children: none
+id:02 rloc16:0x0800 ext-addr:8aa57d2c603fe16c ver:4 - leader - br
+   3-links:{ 46 51 }
+   ip6-addrs:
+       fdde:ad00:beef:0:0:ff:fe00:fc00
+       fdde:ad00:beef:0:0:ff:fe00:800
+       fdde:ad00:beef:0:8a36:a3eb:47ae:a9b0
+       fe80:0:0:0:88a5:7d2c:603f:e16c
+   children:
+       rloc16:0x0803 lq:3, mode:rn
+       rloc16:0x0804 lq:3, mode:rdn
+id:33 rloc16:0x8400 ext-addr:d2e511a146b9e54d ver:4
+   3-links:{ 57 }
+   ip6-addrs:
+       fdde:ad00:beef:0:0:ff:fe00:8400
+       fdde:ad00:beef:0:824:a126:cf19:a9f4
+       fe80:0:0:0:d0e5:11a1:46b9:e54d
+   children: none
+id:51 rloc16:0xcc00 ext-addr:9aab43ababf05352 ver:4
+   3-links:{ 02 46 57 }
+   ip6-addrs:
+       fdde:ad00:beef:0:0:ff:fe00:cc00
+       fdde:ad00:beef:0:2986:bba3:12d0:1dd2
+       fe80:0:0:0:98ab:43ab:abf0:5352
+   children: none
+id:57 rloc16:0xe400 ext-addr:dae9c4c0e9da55ff ver:4
+   3-links:{ 33 51 }
+   ip6-addrs:
+       fdde:ad00:beef:0:0:ff:fe00:e400
+       fdde:ad00:beef:0:87d0:550:bc18:9920
+       fe80:0:0:0:d8e9:c4c0:e9da:55ff
+   children:
+       rloc16:0xe402 lq:3, mode:rn - br
+       rloc16:0xe403 lq:3, mode:rn
+id:46 rloc16:0xb800 ext-addr:fe109d277e0175cc ver:4
+   3-links:{ 02 51 62 }
+   ip6-addrs:
+       fdde:ad00:beef:0:0:ff:fe00:b800
+       fdde:ad00:beef:0:df4d:2994:d85c:c337
+       fe80:0:0:0:fc10:9d27:7e01:75cc
+   children: none
+Done
+```
+
+Discover network topology with children:
+
+```bash
+> meshdiag topology children
+id:02 rloc16:0x0800 ext-addr:8aa57d2c603fe16c ver:4 - parent - leader - br
+   3-links:{ 46 51 }
+   children:
+       rloc16:0x0803 lq:0, mode:rn
+       rloc16:0x0804 lq:0, mode:rdn - me
+id:46 rloc16:0xb800 ext-addr:fe109d277e0175cc ver:4
+   3-links:{ 02 51 62 }
+   children: none
+id:33 rloc16:0x8400 ext-addr:d2e511a146b9e54d ver:4
+   3-links:{ 57 }
+   children: none
+id:51 rloc16:0xcc00 ext-addr:9aab43ababf05352 ver:4
+   3-links:{ 02 46 57 }
+   children: none
+id:57 rloc16:0xe400 ext-addr:dae9c4c0e9da55ff ver:4
+   3-links:{ 33 51 }
+   children:
+       rloc16:0xe402 lq:3, mode:rn - br
+       rloc16:0xe403 lq:3, mode:rn
+id:62 rloc16:0xf800 ext-addr:ce349873897233a5 ver:4 - br
+   3-links:{ 46 }
+   children: none
+```
+
 ### mliid \<iid\>
 
 Set the Mesh Local IID.
@@ -1835,6 +2149,136 @@
 Done
 ```
 
+### nat64 cidr
+
+Gets the IPv4 configured CIDR in the NAT64 translator.
+
+`OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is required.
+
+```bash
+> nat64 cidr
+192.168.255.0/24
+Done
+```
+
+### nat64 disable
+
+Disable NAT64 functions, including the translator and the prefix publishing.
+
+This command will reset the mapping table in the translator (if NAT64 translator is enabled in the build).
+
+`OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` or `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` are required.
+
+```bash
+> nat64 disable
+Done
+```
+
+### nat64 enable
+
+Enable NAT64 functions, including the translator and the prefix publishing.
+
+This command can be called anytime.
+
+`OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` or `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` are required.
+
+```bash
+> nat64 enable
+Done
+```
+
+### nat64 state
+
+Gets the state of NAT64 functions.
+
+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.
+- `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.
+- `Active`: NAT64 translator is enabled and is translating packets.
+
+`OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` or `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` are required.
+
+```bash
+> nat64 state
+PrefixManager: NotRunning
+Translator:    NotRunning
+Done
+
+> nat64 state
+PrefixManager: Idle
+Translator:    NotRunning
+Done
+
+> nat64 state
+PrefixManager: Active
+Translator:    Active
+Done
+```
+
+### nat64 mappings
+
+Get the NAT64 translator mappings.
+
+`OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is required.
+
+```bash
+> nat64 mappings
+|          | Address                   |        | 4 to 6       | 6 to 4       |
++----------+---------------------------+--------+--------------+--------------+
+| ID       | IPv6       | IPv4         | Expiry | Pkts | Bytes | Pkts | Bytes |
++----------+------------+--------------+--------+------+-------+------+-------+
+| 00021cb9 | fdc7::df79 | 192.168.64.2 |  7196s |    6 |   456 |   11 |  1928 |
+|          |                                TCP |    0 |     0 |    0 |     0 |
+|          |                                UDP |    1 |   136 |   16 |  1608 |
+|          |                               ICMP |    5 |   320 |    5 |   320 |
+```
+
+### nat64 counters
+
+Get the NAT64 translator packet and error counters.
+
+`OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is required.
+
+```bash
+> nat64 counters
+|               | 4 to 6                  | 6 to 4                  |
++---------------+-------------------------+-------------------------+
+| Protocol      | Pkts     | Bytes        | Pkts     | Bytes        |
++---------------+----------+--------------+----------+--------------+
+|         Total |       11 |          704 |       11 |          704 |
+|           TCP |        0 |            0 |        0 |            0 |
+|           UDP |        0 |            0 |        0 |            0 |
+|          ICMP |       11 |          704 |       11 |          704 |
+| Errors        | Pkts                    | Pkts                    |
++---------------+-------------------------+-------------------------+
+|         Total |                       8 |                       4 |
+|   Illegal Pkt |                       0 |                       0 |
+|   Unsup Proto |                       0 |                       0 |
+|    No Mapping |                       2 |                       0 |
+Done
+```
+
+### neighbor linkquality
+
+Print link quality info for all neighbors.
+
+```bash
+> neighbor linkquality
+| RLOC16 | Extended MAC     | Frame Error | Msg Error | Avg RSS | Last RSS | Age   |
++--------+------------------+-------------+-----------+---------+----------+-------+
+| 0xe800 | 9e2fa4e1b84f92db |      0.00 % |    0.00 % |     -46 |      -48 |     1 |
+| 0xc001 | 0ad7ed6beaa6016d |      4.67 % |    0.08 % |     -68 |      -72 |    10 |
+Done
+```
+
 ### neighbor list
 
 List RLOC16 of neighbors.
@@ -1859,6 +2303,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.
@@ -1995,7 +2476,7 @@
 
 Get the diagnostic information for a Thread Router as parent.
 
-Note: When operating as a Thread Router, this command will return the cached information from when the device was previously attached as a Thread Child. Returning cached information is necessary to support the Thread Test Harness - Test Scenario 8.2.x requests the former parent (i.e. Joiner Router's) MAC address even if the device has already promoted to a router.
+Note: When operating as a Thread Router when `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is enabled, this command will return the cached information from when the device was previously attached as a Thread Child. Returning cached information is necessary to support the Thread Test Harness - Test Scenario 8.2.x requests the former parent (i.e. Joiner Router's) MAC address even if the device has already promoted to a router.
 
 ```bash
 > parent
@@ -2004,9 +2485,17 @@
 Link Quality In: 3
 Link Quality Out: 3
 Age: 20
+Version: 4
 Done
 ```
 
+Note: When `OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE` is enabled, this command will return two extra lines with information relevant for CSL Receiver operation.
+
+```bash
+CSL clock accuracy: 20
+CSL uncertainty: 5
+```
+
 ### parentpriority
 
 Get the assigned parent priority value, -2 means not assigned.
@@ -2059,10 +2548,11 @@
 Done
 ```
 
-### ping \[-I source\] \<ipaddr\> \[size\] \[count\] \[interval\] \[hoplimit\] \[timeout\]
+### ping \[async\] \[-I source\] \<ipaddr\> \[size\] \[count\] \[interval\] \[hoplimit\] \[timeout\]
 
 Send an ICMPv6 Echo Request.
 
+- async: Use the non-blocking mode. New commands are allowed before the ping process terminates.
 - source: The source IPv6 address of the echo request.
 - size: The number of data bytes to be sent.
 - count: The number of ICMPv6 Echo Requests to be sent.
@@ -2082,6 +2572,18 @@
 Done
 ```
 
+The address can be an IPv4 address, which will be synthesized to an IPv6 address using the preferred NAT64 prefix from the network data.
+
+> Note: The command will return `InvalidState` when the preferred NAT64 prefix is unavailable.
+
+```bash
+> ping 172.17.0.1
+Pinging synthesized IPv6 address: fdde:ad00:beef:2:0:0:ac11:1
+> 16 bytes from fdde:ad00:beef:2:0:0:ac11:1: icmp_seq=5 hlim=64 time=0ms
+1 packets transmitted, 1 packets received. Packet loss = 0.0%. Round-trip min/avg/max = 0/0.0/0 ms.
+Done
+```
+
 ### ping stop
 
 Stop sending ICMPv6 Echo Requests.
@@ -2091,9 +2593,19 @@
 Done
 ```
 
+### platform
+
+Print the current platform
+
+```bash
+> platform
+NRF52840
+Done
+```
+
 ### pollperiod
 
-Get the customized data poll period of sleepy end device (milliseconds). Only for certification test
+Get the customized data poll period of sleepy end device (milliseconds). Only for certification test.
 
 ```bash
 > pollperiod
@@ -2103,13 +2615,23 @@
 
 ### pollperiod \<pollperiod\>
 
-Set the customized data poll period for sleepy end device (milliseconds >= 10ms). Only for certification test
+Set the customized data poll period for sleepy end device (milliseconds >= 10ms). Only for certification test.
 
 ```bash
 > pollperiod 10
 Done
 ```
 
+### pskc
+
+Get pskc in hex format.
+
+```bash
+> pskc
+00000000000000000000000000000000
+Done
+```
+
 ### pskc [-p] \<key\>|\<passphrase\>
 
 With `-p` generate pskc from \<passphrase\> (UTF-8 encoded) together with **current** network name and extended PAN ID, otherwise set pskc as \<key\> (hex format).
@@ -2121,6 +2643,29 @@
 Done
 ```
 
+### pskcref
+
+Get pskc key reference.
+
+`OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE` is required.
+
+```bash
+> pskcref
+0x80000000
+Done
+```
+
+### pskcref \<keyref\>
+
+Set pskc key reference as \<keyref\>.
+
+`OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE` is required.
+
+```bash
+> pskcref 0x20017
+Done
+```
+
 ### preferrouterid \<routerid\>
 
 Prefer a Router ID when solicit router id from Leader.
@@ -2487,6 +3032,25 @@
 Done
 ```
 
+### childrouterlinks
+
+Get the MLE_CHILD_ROUTER_LINKS value.
+
+```bash
+> childrouterlinks
+16
+Done
+```
+
+### childrouterlinks \<number_of_links\>
+
+Set the MLE_CHILD_ROUTER_LINKS value.
+
+```bash
+> childrouterlinks 16
+Done
+```
+
 ### scan \[channel\]
 
 Perform an IEEE 802.15.4 Active Scan.
@@ -2834,6 +3398,63 @@
 >
 ```
 
+### vendor name
+
+This command requires `OPENTHREAD_FTD` or `OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE`.
+
+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
+
+This command requires `OPENTHREAD_FTD` or `OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE`.
+
+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
+
+This command requires `OPENTHREAD_FTD` or `OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE`.
+
+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.
diff --git a/src/cli/README_BR.md b/src/cli/README_BR.md
new file mode 100644
index 0000000..90a50b6
--- /dev/null
+++ b/src/cli/README_BR.md
@@ -0,0 +1,208 @@
+# OpenThread CLI - Border Router (BR)
+
+## Command List
+
+Usage : `br [command] ...`
+
+- [counters](#counters)
+- [disable](#disable)
+- [enable](#enable)
+- [help](#help)
+- [nat64prefix](#nat64prefix)
+- [omrprefix](#omrprefix)
+- [onlinkprefix](#onlinkprefix)
+- [prefixtable](#prefixtable)
+- [rioprf](#rioprf)
+- [state](#state)
+
+## Command Details
+
+### help
+
+Usage: `br help`
+
+Print BR command help menu.
+
+```bash
+> br help
+counters
+disable
+enable
+omrprefix
+onlinkprefix
+prefixtable
+rioprf
+state
+Done
+```
+
+### enable
+
+Usage: `br enable`
+
+Enable the Border Routing functionality.
+
+```bash
+> br enable
+Done
+```
+
+### disable
+
+Usage: `br disable`
+
+Disable the Border Routing functionality.
+
+```bash
+> br disable
+Done
+```
+
+### state
+
+Usage: `br state`
+
+Get the Border Routing state:
+
+- `uninitialized`: Routing Manager is uninitialized.
+- `disabled`: Routing Manager is initialized but disabled.
+- `stopped`: Routing Manager in initialized and enabled but currently stopped.
+- `running`: Routing Manager is initialized, enabled, and running.
+
+```bash
+> br state
+running
+```
+
+### counters
+
+Usage : `br counters`
+
+Get the Border Router counter.
+
+```bash
+> br counters
+Inbound Unicast: Packets 4 Bytes 320
+Inbound Multicast: Packets 0 Bytes 0
+Outbound Unicast: Packets 2 Bytes 160
+Outbound Multicast: Packets 0 Bytes 0
+RA Rx: 4
+RA TxSuccess: 2
+RA TxFailed: 0
+RS Rx: 0
+RS TxSuccess: 2
+RS TxFailed: 0
+Done
+```
+
+### omrprefix
+
+Usage: `br omrprefix [local|favored]`
+
+Get local or favored or both off-mesh-routable prefixes of the Border Router.
+
+```bash
+> br omrprefix
+Local: fdfc:1ff5:1512:5622::/64
+Favored: fdfc:1ff5:1512:5622::/64 prf:low
+Done
+
+> br omrprefix favored
+fdfc:1ff5:1512:5622::/64 prf:low
+Done
+
+> br omrprefix local
+fdfc:1ff5:1512:5622::/64
+Done
+```
+
+### onlinkprefix
+
+Usage: `br onlinkprefix [local|favored]`
+
+Get local or favored or both on-link prefixes of the Border Router.
+
+```bash
+> br onlinkprefix
+Local: fd41:2650:a6f5:0::/64
+Favored: 2600::0:1234:da12::/64
+Done
+
+> br onlinkprefix favored
+2600::0:1234:da12::/64
+Done
+
+> br onlinkprefix local
+fd41:2650:a6f5:0::/64
+Done
+```
+
+### nat64prefix
+
+Usage: `br nat64prefix [local|favored]`
+
+Get local or favored or both NAT64 prefixes of the Border Router.
+
+`OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` is required.
+
+```bash
+> br nat64prefix
+Local: fd14:1078:b3d5:b0b0:0:0::/96
+Favored: fd14:1078:b3d5:b0b0:0:0::/96 prf:low
+Done
+
+> br nat64prefix favored
+fd14:1078:b3d5:b0b0:0:0::/96 prf:low
+Done
+
+> br nat64prefix
+fd14:1078:b3d5:b0b0:0:0::/96
+Done
+```
+
+### prefixtable
+
+Usage: `br prefixtable`
+
+Get the discovered prefixes by Border Routing Manager on the infrastructure link.
+
+```bash
+> br prefixtable
+prefix:fd00:1234:5678:0::/64, on-link:no, ms-since-rx:29526, lifetime:1800, route-prf:med, router:ff02:0:0:0:0:0:0:1
+prefix:1200:abba:baba:0::/64, on-link:yes, ms-since-rx:29527, lifetime:1800, preferred:1800, router:ff02:0:0:0:0:0:0:1
+Done
+```
+
+### rioprf
+
+Usage: `br rioprf`
+
+Get the preference used when advertising Route Info Options (e.g., for discovered OMR prefixes) in emitted Router Advertisement message.
+
+```bash
+> br rioprf
+med
+Done
+```
+
+### rioprf \<prf\>
+
+Usage: `br rioprf high|med|low`
+
+Set the preference (which may be 'high', 'med', or 'low') to use when advertising Route Info Options (e.g., for discovered OMR prefixes) in emitted Router Advertisement message.
+
+```bash
+> br rioprf low
+Done
+```
+
+### rioprf clear
+
+Usage: `br rioprf clear`
+
+Clear a previously set preference value for advertising Route Info Options (e.g., for discovered OMR prefixes) in emitted Router Advertisement message. When cleared BR will use device's role to determine the RIO preference: Medium preference when in router/leader role and low preference when in child role.
+
+```bash
+> br rioprf clear
+Done
+```
diff --git a/src/cli/README_DATASET.md b/src/cli/README_DATASET.md
index 87cd4d2..df1b274 100644
--- a/src/cli/README_DATASET.md
+++ b/src/cli/README_DATASET.md
@@ -36,15 +36,15 @@
    Done
    > dataset
    Active Timestamp: 1
-   Channel: 13
+   Channel: 15
    Channel Mask: 0x07fff800
-   Ext PAN ID: d63e8e3e495ebbc3
-   Mesh Local Prefix: fd3d:b50b:f96d:722d::/64
-   Network Key: dfd34f0f05cad978ec4e32b0413038ff
-   Network Name: OpenThread-8f28
-   PAN ID: 0x8f28
-   PSKc: c23a76e98f1a6483639b1ac1271e2e27
-   Security Policy: 0, onrc
+   Ext PAN ID: 39758ec8144b07fb
+   Mesh Local Prefix: fdf1:f1ad:d079:7dc0::/64
+   Network Key: f366cec7a446bab978d90d27abe38f23
+   Network Name: OpenThread-5938
+   PAN ID: 0x5938
+   PSKc: 3ca67c969efb0d0c74a4d8ee923b576c
+   Security Policy: 672 onrc
    Done
    ```
 
@@ -95,15 +95,15 @@
    ```bash
    > dataset active
    Active Timestamp: 1
-   Channel: 13
+   Channel: 15
    Channel Mask: 0x07fff800
-   Ext PAN ID: d63e8e3e495ebbc3
-   Mesh Local Prefix: fd3d:b50b:f96d:722d::/64
-   Network Key: dfd34f0f05cad978ec4e32b0413038ff
-   Network Name: OpenThread-8f28
-   PAN ID: 0x8f28
-   PSKc: c23a76e98f1a6483639b1ac1271e2e27
-   Security Policy: 0, onrc
+   Ext PAN ID: 39758ec8144b07fb
+   Mesh Local Prefix: fdf1:f1ad:d079:7dc0::/64
+   Network Key: f366cec7a446bab978d90d27abe38f23
+   Network Name: OpenThread-5938
+   PAN ID: 0x5938
+   PSKc: 3ca67c969efb0d0c74a4d8ee923b576c
+   Security Policy: 672 onrc
    Done
    ```
 
@@ -129,6 +129,7 @@
 - [pendingtimestamp](#pendingtimestamp)
 - [pskc](#pskc)
 - [securitypolicy](#securitypolicy)
+- [tlvs](#tlvs)
 
 ## Command Details
 
@@ -160,6 +161,8 @@
 pendingtimestamp
 pskc
 securitypolicy
+set
+tlvs
 Done
 ```
 
@@ -172,15 +175,15 @@
 ```bash
 > dataset active
 Active Timestamp: 1
-Channel: 13
+Channel: 15
 Channel Mask: 0x07fff800
-Ext PAN ID: d63e8e3e495ebbc3
-Mesh Local Prefix: fd3d:b50b:f96d:722d::/64
-Network Key: dfd34f0f05cad978ec4e32b0413038ff
-Network Name: OpenThread-8f28
-PAN ID: 0x8f28
-PSKc: c23a76e98f1a6483639b1ac1271e2e27
-Security Policy: 0, onrc
+Ext PAN ID: 39758ec8144b07fb
+Mesh Local Prefix: fdf1:f1ad:d079:7dc0::/64
+Network Key: f366cec7a446bab978d90d27abe38f23
+Network Name: OpenThread-5938
+PAN ID: 0x5938
+PSKc: 3ca67c969efb0d0c74a4d8ee923b576c
+Security Policy: 672 onrc
 Done
 ```
 
@@ -188,7 +191,7 @@
 
 ```bash
 > dataset active -x
-0e080000000000010000000300001035060004001fffe002084eb74ab03c56e6d00708fdc7fe165c83a67805108e2104f183e698da87e96efc1e45aa51030f4f70656e5468726561642d383631310102861104108d6273023d82c841eff0e68db86f35740c030000ff
+0e080000000000010000000300000f35060004001fffe0020839758ec8144b07fb0708fdf1f1add0797dc00510f366cec7a446bab978d90d27abe38f23030f4f70656e5468726561642d353933380102593804103ca67c969efb0d0c74a4d8ee923b576c0c0402a0f7f8
 Done
 ```
 
@@ -431,17 +434,17 @@
 ```bash
 > dataset pending
 Pending Timestamp: 2
-Active Timestamp: 15
-Channel: 16
+Active Timestamp: 1
+Channel: 26
 Channel Mask: 0x07fff800
 Delay: 58706
-Ext PAN ID: d63e8e3e495ebbc3
-Mesh Local Prefix: fd3d:b50b:f96d:722d::/64
-Network Key: dfd34f0f05cad978ec4e32b0413038ff
-Network Name: OpenThread-8f28
-PAN ID: 0x8f28
-PSKc: c23a76e98f1a6483639b1ac1271e2e27
-Security Policy: 0, onrc
+Ext PAN ID: a74182f4d3f4de41
+Mesh Local Prefix: fd46:c1b9:e159:5574::/64
+Network Key: ed916e454d96fd00184f10a6f5c9e1d3
+Network Name: OpenThread-bff8
+PAN ID: 0xbff8
+PSKc: 264f78414adc683191863d968f72d1b7
+Security Policy: 672 onrc
 Done
 ```
 
@@ -449,7 +452,7 @@
 
 ```bash
 > dataset pending -x
-0e080000000000010000000300001035060004001fffe002084eb74ab03c56e6d00708fdc7fe165c83a67805108e2104f183e698da87e96efc1e45aa51030f4f70656e5468726561642d383631310102861104108d6273023d82c841eff0e68db86f35740c030000ff
+0e0800000000000100003308000000000002000034040000b512000300001a35060004001fffe00208a74182f4d3f4de410708fd46c1b9e15955740510ed916e454d96fd00184f10a6f5c9e1d3030f4f70656e5468726561642d626666380102bff80410264f78414adc683191863d968f72d1b70c0402a0f7f8
 Done
 ```
 
@@ -530,13 +533,38 @@
 Set the Active Operational Dataset using hex-encoded TLVs.
 
 ```bash
-dataset set active 0e080000000000010000000300001035060004001fffe002084eb74ab03c56e6d00708fdc7fe165c83a67805108e2104f183e698da87e96efc1e45aa51030f4f70656e5468726561642d383631310102861104108d6273023d82c841eff0e68db86f35740c030000ff
+> dataset set active 0e080000000000010000000300000f35060004001fffe0020839758ec8144b07fb0708fdf1f1add0797dc00510f366cec7a446bab978d90d27abe38f23030f4f70656e5468726561642d353933380102593804103ca67c969efb0d0c74a4d8ee923b576c0c0402a0f7f8
 Done
 ```
 
 Set the Pending Operational Dataset using hex-encoded TLVs.
 
 ```bash
-dataset set pending 0e080000000000010000000300001035060004001fffe002084eb74ab03c56e6d00708fdc7fe165c83a67805108e2104f183e698da87e96efc1e45aa51030f4f70656e5468726561642d383631310102861104108d6273023d82c841eff0e68db86f35740c030000ff
+> dataset set pending 0e0800000000000100003308000000000002000034040000b512000300001a35060004001fffe00208a74182f4d3f4de410708fd46c1b9e15955740510ed916e454d96fd00184f10a6f5c9e1d3030f4f70656e5468726561642d626666380102bff80410264f78414adc683191863d968f72d1b70c0402a0f7f8
+Done
+```
+
+### tlvs
+
+Usage: `dataset tlvs`
+
+Convert the Operational Dataset to hex-encoded TLVs.
+
+```bash
+> dataset
+Active Timestamp: 1
+Channel: 22
+Channel Mask: 0x07fff800
+Ext PAN ID: d196fa2040e973b6
+Mesh Local Prefix: fdbb:c310:c48f:3a39::/64
+Network Key: 9929154dbc363218bcd22f907caf5c15
+Network Name: OpenThread-de2b
+PAN ID: 0xde2b
+PSKc: 15b2c16f7ba92ed4bc7b1ee054f1553f
+Security Policy: 672 onrc
+Done
+
+> dataset tlvs
+0e080000000000010000000300001635060004001fffe00208d196fa2040e973b60708fdbbc310c48f3a3905109929154dbc363218bcd22f907caf5c15030f4f70656e5468726561642d646532620102de2b041015b2c16f7ba92ed4bc7b1ee054f1553f0c0402a0f7f8
 Done
 ```
diff --git a/src/cli/README_HISTORY.md b/src/cli/README_HISTORY.md
index e3ea55d..268cf7d 100644
--- a/src/cli/README_HISTORY.md
+++ b/src/cli/README_HISTORY.md
@@ -17,6 +17,7 @@
 - [netinfo](#netinfo)
 - [prefix](#prefix)
 - [route](#route)
+- [router](#router)
 - [rx](#rx)
 - [rxtx](#rxtx)
 - [tx](#tx)
@@ -56,6 +57,7 @@
 netinfo
 prefix
 route
+router
 rx
 rxtx
 tx
@@ -298,7 +300,7 @@
 00:06:01.711 -> event:Added prefix:fd00:dead:beef:1::/64 flags:paros pref:med rloc16:0x8800
 ```
 
-### prefix
+### route
 
 Usage `history route [list] [<num-entries>]`
 
@@ -337,6 +339,82 @@
 Done
 ```
 
+### router
+
+Usage `history router [list] [<num-entries>]`
+
+Print the route table history. Each item provides:
+
+- Event (`Added`, `Removed`, `NextHopChnaged`, `CostChanged`)
+- Router ID and RLOC16 of router
+- Next Hop (Router ID and RLOC16) - `none` if no next hop.
+- Path cost (old `->` new) - `inf` to indicate infinite path cost.
+
+Print the history as a table.
+
+```bash
+> history router
+| Age                  | Event          | ID (RLOC16) | Next Hop    | Path Cost  |
++----------------------+----------------+-------------+-------------+------------+
+|         00:00:05.258 | NextHopChanged |  7 (0x1c00) | 34 (0x8800) | inf ->   3 |
+|         00:00:08.604 | NextHopChanged | 34 (0x8800) | 34 (0x8800) | inf ->   2 |
+|         00:00:08.604 | Added          |  7 (0x1c00) |        none | inf -> inf |
+|         00:00:11.931 | Added          | 34 (0x8800) |        none | inf -> inf |
+|         00:00:14.948 | Removed        | 59 (0xec00) |        none | inf -> inf |
+|         00:00:14.948 | Removed        | 54 (0xd800) |        none | inf -> inf |
+|         00:00:14.948 | Removed        | 34 (0x8800) |        none | inf -> inf |
+|         00:00:14.948 | Removed        |  7 (0x1c00) |        none | inf -> inf |
+|         00:00:54.795 | NextHopChanged | 59 (0xec00) | 34 (0x8800) |   1 ->   5 |
+|         00:02:33.735 | NextHopChanged | 54 (0xd800) |        none |  15 -> inf |
+|         00:03:10.915 | CostChanged    | 54 (0xd800) | 34 (0x8800) |  13 ->  15 |
+|         00:03:45.716 | NextHopChanged | 54 (0xd800) | 34 (0x8800) |  15 ->  13 |
+|         00:03:46.188 | CostChanged    | 54 (0xd800) | 59 (0xec00) |  13 ->  15 |
+|         00:04:19.124 | CostChanged    | 54 (0xd800) | 59 (0xec00) |  11 ->  13 |
+|         00:04:52.008 | CostChanged    | 54 (0xd800) | 59 (0xec00) |   9 ->  11 |
+|         00:05:23.176 | CostChanged    | 54 (0xd800) | 59 (0xec00) |   7 ->   9 |
+|         00:05:51.081 | CostChanged    | 54 (0xd800) | 59 (0xec00) |   5 ->   7 |
+|         00:06:48.721 | CostChanged    | 54 (0xd800) | 59 (0xec00) |   3 ->   5 |
+|         00:07:13.792 | NextHopChanged | 54 (0xd800) | 59 (0xec00) |   1 ->   3 |
+|         00:09:28.681 | NextHopChanged |  7 (0x1c00) | 34 (0x8800) | inf ->   3 |
+|         00:09:31.882 | Added          |  7 (0x1c00) |        none | inf -> inf |
+|         00:09:51.240 | NextHopChanged | 54 (0xd800) | 54 (0xd800) | inf ->   1 |
+|         00:09:54.204 | Added          | 54 (0xd800) |        none | inf -> inf |
+|         00:10:20.645 | NextHopChanged | 34 (0x8800) | 34 (0x8800) | inf ->   2 |
+|         00:10:24.242 | NextHopChanged | 59 (0xec00) | 59 (0xec00) | inf ->   1 |
+|         00:10:24.242 | Added          | 34 (0x8800) |        none | inf -> inf |
+|         00:10:41.900 | NextHopChanged | 59 (0xec00) |        none |   1 -> inf |
+|         00:10:42.480 | Added          |  3 (0x0c00) |  3 (0x0c00) | inf -> inf |
+|         00:10:43.614 | Added          | 59 (0xec00) | 59 (0xec00) | inf ->   1 |
+Done
+```
+
+Print the history as a list (last 20 entries).
+
+```bash
+> history router list 20
+00:00:06.959 -> event:NextHopChanged router:7(0x1c00) nexthop:34(0x8800) old-cost:inf new-cost:3
+00:00:10.305 -> event:NextHopChanged router:34(0x8800) nexthop:34(0x8800) old-cost:inf new-cost:2
+00:00:10.305 -> event:Added router:7(0x1c00) nexthop:none old-cost:inf new-cost:inf
+00:00:13.632 -> event:Added router:34(0x8800) nexthop:none old-cost:inf new-cost:inf
+00:00:16.649 -> event:Removed router:59(0xec00) nexthop:none old-cost:inf new-cost:inf
+00:00:16.649 -> event:Removed router:54(0xd800) nexthop:none old-cost:inf new-cost:inf
+00:00:16.649 -> event:Removed router:34(0x8800) nexthop:none old-cost:inf new-cost:inf
+00:00:16.649 -> event:Removed router:7(0x1c00) nexthop:none old-cost:inf new-cost:inf
+00:00:56.496 -> event:NextHopChanged router:59(0xec00) nexthop:34(0x8800) old-cost:1 new-cost:5
+00:02:35.436 -> event:NextHopChanged router:54(0xd800) nexthop:none old-cost:15 new-cost:inf
+00:03:12.616 -> event:CostChanged router:54(0xd800) nexthop:34(0x8800) old-cost:13 new-cost:15
+00:03:47.417 -> event:NextHopChanged router:54(0xd800) nexthop:34(0x8800) old-cost:15 new-cost:13
+00:03:47.889 -> event:CostChanged router:54(0xd800) nexthop:59(0xec00) old-cost:13 new-cost:15
+00:04:20.825 -> event:CostChanged router:54(0xd800) nexthop:59(0xec00) old-cost:11 new-cost:13
+00:04:53.709 -> event:CostChanged router:54(0xd800) nexthop:59(0xec00) old-cost:9 new-cost:11
+00:05:24.877 -> event:CostChanged router:54(0xd800) nexthop:59(0xec00) old-cost:7 new-cost:9
+00:05:52.782 -> event:CostChanged router:54(0xd800) nexthop:59(0xec00) old-cost:5 new-cost:7
+00:06:50.422 -> event:CostChanged router:54(0xd800) nexthop:59(0xec00) old-cost:3 new-cost:5
+00:07:15.493 -> event:NextHopChanged router:54(0xd800) nexthop:59(0xec00) old-cost:1 new-cost:3
+00:09:30.382 -> event:NextHopChanged router:7(0x1c00) nexthop:34(0x8800) old-cost:inf new-cost:3
+Done
+```
+
 ### rx
 
 Usage `history rx [list] [<num-entries>]`
diff --git a/src/cli/README_NETDATA.md b/src/cli/README_NETDATA.md
index 68c3b43..5fff19f 100644
--- a/src/cli/README_NETDATA.md
+++ b/src/cli/README_NETDATA.md
@@ -142,6 +142,8 @@
 ## Command List
 
 - [help](#help)
+- [length](#length)
+- [maxlength](#maxlength)
 - [publish](#publish)
 - [register](#register)
 - [show](#show)
@@ -158,7 +160,8 @@
 
 ```bash
 > netdata help
-help
+length
+maxlength
 publish
 register
 show
@@ -167,6 +170,41 @@
 Done
 ```
 
+### length
+
+Usage: `netdata length`
+
+Get the current length of (number of bytes) Partition's Thread Network Data.
+
+```bash
+> netdata length
+23
+Done
+```
+
+### maxlength
+
+Usage: `netdata maxlength`
+
+Get the maximum observed length of the Thread Network Data since OT stack initialization or since the last call to `netdata maxlength reset`.
+
+```bash
+> netdata maxlength
+40
+Done
+```
+
+### maxlength reset
+
+Usage: `netdata maxlength reset`
+
+Reset the tracked maximum length of the Thread Network Data.
+
+```bash
+> netdata maxlength reset
+Done
+```
+
 ### publish
 
 The Network Data Publisher provides mechanisms to limit the number of similar Service and/or Prefix (on-mesh prefix or external route) entries in the Thread Network Data by monitoring the Network Data and managing if or when to add or remove entries.
@@ -231,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`
@@ -246,14 +299,61 @@
 
 Usage: `netdata show [local] [-x]`
 
+Print entries in Network Data, on-mesh prefixes, external routes, services, and 6LoWPAN context information.
+
+On-mesh prefixes are listed under `Prefixes` header:
+
+- The on-mesh prefix
+- Flags
+  - p: Preferred flag
+  - a: Stateless IPv6 Address Autoconfiguration flag
+  - d: DHCPv6 IPv6 Address Configuration flag
+  - c: DHCPv6 Other Configuration flag
+  - r: Default Route flag
+  - o: On Mesh flag
+  - s: Stable flag
+  - n: Nd Dns flag
+  - D: Domain Prefix flag (only available for Thread 1.2).
+- Preference `high`, `med`, or `low`
+- RLOC16 of device which added the on-mesh prefix
+
+External Routes are listed under `Routes` header:
+
+- The route prefix
+- Flags
+  - s: Stable flag
+  - n: NAT64 flag
+- Preference `high`, `med`, or `low`
+- RLOC16 of device which added the route prefix
+
+Service entries are listed under `Services` header:
+
+- Enterprise number
+- Service data (as hex bytes)
+- Server data (as hex bytes)
+- Flags
+  - s: Stable flag
+- RLOC16 of devices which added the service entry
+
+6LoWPAN Context IDs are listed under `Contexts` header:
+
+- The prefix
+- Context ID
+- Compress flag (`c` if marked or `-` otherwise).
+
 Print Network Data received from the Leader.
 
 ```bash
 > netdata show
 Prefixes:
-fd00:dead:beef:cafe::/64 paros med dc00
+fd00:dead:beef:cafe::/64 paros med a000
 Routes:
+fd00:1234:0:0::/64 s med a000
+fd00:4567:0:0::/64 s med 8000
 Services:
+44970 5d fddead00beef00007bad0069ce45948504d2 s a000
+Contexts:
+fd00:dead:beef:cafe::/64 1 c
 Done
 ```
 
diff --git a/src/cli/README_SRP.md b/src/cli/README_SRP.md
index a0ab5ba..ce2139f 100644
--- a/src/cli/README_SRP.md
+++ b/src/cli/README_SRP.md
@@ -131,7 +131,10 @@
     port: 12345
     priority: 0
     weight: 0
-    TXT: 00
+    ttl: 7200
+    lease: 7200
+    key-lease: 1209600
+    TXT: []
     host: my-host.default.service.arpa.
     addresses: [fded:5114:8263:1fe1:44f9:cc06:4a2d:534]
 Done
diff --git a/src/cli/README_SRP_SERVER.md b/src/cli/README_SRP_SERVER.md
index 05cb9bc..ad87182 100644
--- a/src/cli/README_SRP_SERVER.md
+++ b/src/cli/README_SRP_SERVER.md
@@ -170,6 +170,8 @@
 
 Print information of all registered services.
 
+The TXT record is displayed as an array of entries. If an entry has a key, the key will be printed in ASCII format. The value portion will always be printed as hex bytes.
+
 ```bash
 > srp server service
 srp-api-test-1._ipps._tcp.default.service.arpa.
@@ -178,7 +180,10 @@
     port: 49152
     priority: 0
     weight: 0
-    TXT: 0130
+    ttl: 7200
+    lease: 7200
+    key-lease: 1209600
+    TXT: [616263, xyz=585960]
     host: srp-api-test-1.default.service.arpa.
     addresses: [fdde:ad00:beef:0:0:ff:fe00:fc10]
 srp-api-test-0._ipps._tcp.default.service.arpa.
@@ -187,7 +192,10 @@
     port: 49152
     priority: 0
     weight: 0
-    TXT: 0130
+    ttl: 3600
+    lease: 3600
+    key-lease: 1209600
+    TXT: [616263, xyz=585960]
     host: srp-api-test-0.default.service.arpa.
     addresses: [fdde:ad00:beef:0:0:ff:fe00:fc10]
 Done
diff --git a/src/cli/README_TCP.md b/src/cli/README_TCP.md
index c1aba71..3869570 100644
--- a/src/cli/README_TCP.md
+++ b/src/cli/README_TCP.md
@@ -109,7 +109,7 @@
 
 If the connection establishment is successful, the resulting TCP connection is associated with the example TCP endpoint.
 
-- ip: the peer's IPv6 address.
+- ip: the peer's IP address.
 - port: the peer's TCP port.
 
 ```bash
@@ -118,6 +118,16 @@
 TCP: Connection established
 ```
 
+The address can be an IPv4 address, which will be synthesized to an IPv6 address using the preferred NAT64 prefix from the network data.
+
+> Note: The command will return `InvalidState` when the preferred NAT64 prefix is unavailable.
+
+```bash
+> tcp connect 172.17.0.1 1234
+Connecting to synthesized IPv6 address: fdde:ad00:beef:2:0:0:ac11:1
+Done
+```
+
 ### deinit
 
 Deinitializes the example TCP listener and the example TCP endpoint.
@@ -147,14 +157,19 @@
 Done
 ```
 
-### init [\<size\>]
+### init [\<mode\>]&nbsp;[\<size\>]
 
 Initializes the example TCP listener and the example TCP endpoint.
 
+- mode: this specifies the buffering strategy and whether to use TLS. The possible values are "linked", "circular" (default), and "tls".
 - size: the size of the receive buffer to associate with the example TCP endpoint. If left unspecified, the maximum size is used.
 
+If "tls" is used, then the TLS protocol will be used for the connection (on top of TCP). When communicating over TCP between two nodes, either both should use TLS or neither should (a non-TLS endpoint cannot communicate with a TLS endpoint). The first two options, "linked" and "circular", specify that TLS should not be used and specify a buffering strategy to use with TCP; two endpoints of a TCP connection may use different buffering strategies.
+
+The behaviors of "linked" and "circular" buffering are identical, but the option is provided so that users of TCP can inspect the code to see an example of using the two buffering strategies.
+
 ```bash
-> tcp init
+> tcp init tls
 Done
 ```
 
diff --git a/src/cli/README_UDP.md b/src/cli/README_UDP.md
index 51cd8cc..14444cd 100644
--- a/src/cli/README_UDP.md
+++ b/src/cli/README_UDP.md
@@ -96,7 +96,7 @@
 
 Specifies the peer with which the socket is to be associated.
 
-- ip: the peer's IPv6 address.
+- ip: the peer's IP address.
 - port: the peer's UDP port.
 
 ```bash
@@ -104,6 +104,16 @@
 Done
 ```
 
+The address can be an IPv4 address, which will be synthesized to an IPv6 address using the preferred NAT64 prefix from the network data.
+
+> Note: The command will return `InvalidState` when the preferred NAT64 prefix is unavailable.
+
+```bash
+> udp connect 172.17.0.1 1234
+Connecting to synthesized IPv6 address: fdde:ad00:beef:2:0:0:ac11:1
+Done
+```
+
 ### linksecurity
 
 Indicates whether the link security is enabled or disabled.
@@ -145,7 +155,7 @@
 
 Send a UDP message.
 
-- ip: the IPv6 destination address.
+- ip: the destination address.
 - port: the UDP destination port.
 - message: the message to send.
 
@@ -154,6 +164,16 @@
 Done
 ```
 
+The address can be an IPv4 address, which will be synthesized to an IPv6 address using the preferred NAT64 prefix from the network data.
+
+> Note: The command will return `InvalidState` when the preferred NAT64 prefix is unavailable.
+
+```bash
+> udp send 172.17.0.1 1234
+Sending to synthesized IPv6 address: fdde:ad00:beef:2:0:0:ac11:1
+Done
+```
+
 ### send \<ip\> \<port\> \<type\> \<value\>
 
 Send a few bytes over UDP.
diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp
index 7178527..6d11023 100644
--- a/src/cli/cli.cpp
+++ b/src/cli/cli.cpp
@@ -37,6 +37,7 @@
 #include <stdlib.h>
 #include <string.h>
 
+#include <openthread/child_supervision.h>
 #include <openthread/diag.h>
 #include <openthread/dns.h>
 #include <openthread/icmp6.h>
@@ -44,6 +45,7 @@
 #include <openthread/logging.h>
 #include <openthread/ncp.h>
 #include <openthread/thread.h>
+#include "common/num_utils.hpp"
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 #include <openthread/network_time.h>
 #endif
@@ -57,9 +59,6 @@
 #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
 #include <openthread/server.h>
 #endif
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-#include <openthread/child_supervision.h>
-#endif
 #if OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE
 #include <openthread/platform/misc.h>
 #endif
@@ -84,6 +83,9 @@
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
 #include <openthread/trel.h>
 #endif
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE || OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+#include <openthread/nat64.h>
+#endif
 
 #include "common/new.hpp"
 #include "common/string.hpp"
@@ -96,50 +98,56 @@
 static OT_DEFINE_ALIGNED_VAR(sInterpreterRaw, sizeof(Interpreter), uint64_t);
 
 Interpreter::Interpreter(Instance *aInstance, otCliOutputCallback aCallback, void *aContext)
-    : Output(aInstance, aCallback, aContext)
-    , mUserCommands(nullptr)
-    , mUserCommandsLength(0)
+    : OutputImplementer(aCallback, aContext)
+    , Output(aInstance, *this)
     , mCommandIsPending(false)
     , mTimer(*aInstance, HandleTimer, this)
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
 #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE
     , mSntpQueryingInProgress(false)
 #endif
-    , mDataset(*this)
-    , mNetworkData(*this)
-    , mUdp(*this)
+    , mDataset(aInstance, *this)
+    , mNetworkData(aInstance, *this)
+    , mUdp(aInstance, *this)
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    , mBr(aInstance, *this)
+#endif
 #if OPENTHREAD_CONFIG_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_TCP_ENABLE
-    , mTcp(*this)
+    , mTcp(aInstance, *this)
 #endif
 #if OPENTHREAD_CONFIG_COAP_API_ENABLE
-    , mCoap(*this)
+    , mCoap(aInstance, *this)
 #endif
 #if OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE
-    , mCoapSecure(*this)
+    , mCoapSecure(aInstance, *this)
 #endif
 #if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD
-    , mCommissioner(*this)
+    , mCommissioner(aInstance, *this)
 #endif
 #if OPENTHREAD_CONFIG_JOINER_ENABLE
-    , mJoiner(*this)
+    , mJoiner(aInstance, *this)
 #endif
 #if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
-    , mSrpClient(*this)
+    , mSrpClient(aInstance, *this)
 #endif
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
-    , mSrpServer(*this)
+    , mSrpServer(aInstance, *this)
 #endif
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-    , mHistory(*this)
+    , mHistory(aInstance, *this)
 #endif
 #if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE
     , mLocateInProgress(false)
 #endif
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    , mLinkMetricsQueryInProgress(false)
+#endif
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
 {
-#if OPENTHREAD_FTD
-    otThreadSetDiscoveryRequestCallback(GetInstancePtr(), &Interpreter::HandleDiscoveryRequest, this);
+#if (OPENTHREAD_FTD || OPENTHREAD_MTD) && OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK
+    otIp6SetReceiveCallback(GetInstancePtr(), &Interpreter::HandleIp6Receive, this);
 #endif
+    memset(&mUserCommands, 0, sizeof(mUserCommands));
 
     OutputPrompt();
 }
@@ -156,7 +164,7 @@
     }
     else
     {
-        OutputLine("Error %d: %s", aError, otThreadErrorToString(aError));
+        OutputLine("Error %u: %s", aError, otThreadErrorToString(aError));
     }
 
     mCommandIsPending = false;
@@ -200,7 +208,7 @@
 template <> otError Interpreter::Process<Cmd("diag")>(Arg aArgs[])
 {
     otError error;
-    char *  args[kMaxArgs];
+    char   *args[kMaxArgs];
     char    output[OPENTHREAD_CONFIG_DIAG_OUTPUT_BUFFER_SIZE];
 
     // all diagnostics related features are processed within diagnostics module
@@ -224,7 +232,7 @@
     }
     else if (aArgs[0] == "api")
     {
-        OutputLine("%d", OPENTHREAD_API_VERSION);
+        OutputLine("%u", OPENTHREAD_API_VERSION);
     }
     else
     {
@@ -286,27 +294,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);
-            mUserCommands[i].mCommand(mUserCommandsContext, Arg::GetArgsLength(aArgs) - 1, args + 1);
-            error = OT_ERROR_NONE;
-            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
@@ -333,7 +356,7 @@
 otError Interpreter::ParseJoinerDiscerner(Arg &aArg, otJoinerDiscerner &aDiscerner)
 {
     otError error;
-    char *  separator;
+    char   *separator;
 
     VerifyOrExit(!aArg.IsEmpty(), error = OT_ERROR_INVALID_ARGS);
 
@@ -355,7 +378,7 @@
 otError Interpreter::ParsePingInterval(const Arg &aArg, uint32_t &aInterval)
 {
     otError        error    = OT_ERROR_NONE;
-    const char *   string   = aArg.GetCString();
+    const char    *string   = aArg.GetCString();
     const uint32_t msFactor = 1000;
     uint32_t       factor   = msFactor;
 
@@ -450,11 +473,33 @@
     return str;
 }
 
-#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-template <> otError Interpreter::Process<Cmd("history")>(Arg aArgs[])
+otError Interpreter::ParseToIp6Address(otInstance   *aInstance,
+                                       const Arg    &aArg,
+                                       otIp6Address &aAddress,
+                                       bool         &aSynthesized)
 {
-    return mHistory.Process(aArgs);
+    Error error = kErrorNone;
+
+    VerifyOrExit(!aArg.IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    error        = aArg.ParseAsIp6Address(aAddress);
+    aSynthesized = false;
+    if (error != kErrorNone)
+    {
+        // It might be an IPv4 address, let's have a try.
+        otIp4Address ip4Address;
+
+        // Do not touch the error value if we failed to parse it as an IPv4 address.
+        SuccessOrExit(aArg.ParseAsIp4Address(ip4Address));
+        SuccessOrExit(error = otNat64SynthesizeIp6Address(aInstance, &ip4Address, &aAddress));
+        aSynthesized = true;
+    }
+
+exit:
+    return error;
 }
+
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+template <> otError Interpreter::Process<Cmd("history")>(Arg aArgs[]) { return mHistory.Process(aArgs); }
 #endif
 
 #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
@@ -510,181 +555,304 @@
 #endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
-template <> otError Interpreter::Process<Cmd("br")>(Arg aArgs[])
+template <> otError Interpreter::Process<Cmd("br")>(Arg aArgs[]) { return mBr.Process(aArgs); }
+#endif
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE || OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+template <> otError Interpreter::Process<Cmd("nat64")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
     bool    enable;
 
+    if (aArgs[0].IsEmpty())
+    {
+        ExitNow(error = OT_ERROR_INVALID_COMMAND);
+    }
     /**
-     * @cli br (enable,disable)
+     * @cli nat64 (enable,disable)
      * @code
-     * br enable
+     * nat64 enable
      * Done
      * @endcode
      * @code
-     * br disable
+     * nat64 disable
      * Done
      * @endcode
+     * @cparam nat64 @ca{enable|disable}
      * @par api_copy
-     * #otBorderRoutingSetEnabled
+     * #otNat64SetEnabled
+     *
      */
     if (ParseEnableOrDisable(aArgs[0], enable) == OT_ERROR_NONE)
     {
-        SuccessOrExit(error = otBorderRoutingSetEnabled(GetInstancePtr(), enable));
+        otNat64SetEnabled(GetInstancePtr(), enable);
     }
     /**
-     * @cli br omrprefix
+     * @cli nat64 state
      * @code
-     * br omrprefix
-     * fdfc:1ff5:1512:5622::/64
+     * nat64 state
+     * PrefixManager: Active
+     * Translator: Active
      * Done
      * @endcode
-     * @par api_copy
-     * #otBorderRoutingGetOmrPrefix
-     */
-    else if (aArgs[0] == "omrprefix")
-    {
-        otIp6Prefix omrPrefix;
-
-        SuccessOrExit(error = otBorderRoutingGetOmrPrefix(GetInstancePtr(), &omrPrefix));
-        OutputIp6PrefixLine(omrPrefix);
-    }
-    /**
-     * @cli br favoredomrprefix
-     * @code
-     * br favoredomrprefix
-     * fdfc:1ff5:1512:5622::/64 prf:low
-     * Done
-     * @endcode
-     * @par api_copy
-     * #otBorderRoutingGetFavoredOmrPrefix
-     */
-    else if (aArgs[0] == "favoredomrprefix")
-    {
-        otIp6Prefix       prefix;
-        otRoutePreference preference;
-
-        SuccessOrExit(error = otBorderRoutingGetFavoredOmrPrefix(GetInstancePtr(), &prefix, &preference));
-        OutputIp6Prefix(prefix);
-        OutputLine(" prf:%s", PreferenceToString(preference));
-    }
-    /**
-     * @cli br onlinkprefix
-     * @code
-     * br onlinkprefix
-     * fd41:2650:a6f5:0::/64
-     * Done
-     * @endcode
-     * @par api_copy
-     * #otBorderRoutingGetOnLinkPrefix
-     */
-    else if (aArgs[0] == "onlinkprefix")
-    {
-        otIp6Prefix onLinkPrefix;
-
-        SuccessOrExit(error = otBorderRoutingGetOnLinkPrefix(GetInstancePtr(), &onLinkPrefix));
-        OutputIp6PrefixLine(onLinkPrefix);
-    }
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
-    /**
-     * @cli br nat64prefix
-     * @code
-     * br nat64prefix
-     * fd14:1078:b3d5:b0b0:0:0::/96
-     * Done
-     * @endcode
-     * @par api_copy
-     * #otBorderRoutingGetNat64Prefix
-     */
-    else if (aArgs[0] == "nat64prefix")
-    {
-        otIp6Prefix nat64Prefix;
-
-        SuccessOrExit(error = otBorderRoutingGetNat64Prefix(GetInstancePtr(), &nat64Prefix));
-        OutputIp6PrefixLine(nat64Prefix);
-    }
-#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
-    /**
-     * @cli br rioprf (high,med,low)
-     * @code
-     * br rioprf
-     * med
-     * Done
-     * @endcode
-     * @code
-     * br rioprf low
-     * Done
-     * @endcode
-     * @cparam br rioprf [@ca{high}|@ca{med}|@ca{low}]
-     * @par api_copy
-     * #otBorderRoutingSetRouteInfoOptionPreference
+     * @par
+     * Gets the state of NAT64 functions.
+     * @par
+     * `PrefixManager` state is available when `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` is enabled.
+     * `Translator` state is available when `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled.
+     * @par
+     * When `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` is enabled, `PrefixManager` returns one of the following
+     * states:
+     * - `Disabled`: NAT64 prefix manager is disabled.
+     * - `NotRunning`: NAT64 prefix manager is enabled, but is not running. This could mean that the routing manager is
+     *   disabled.
+     * - `Idle`: NAT64 prefix manager is enabled and is running, but is not publishing a NAT64 prefix. This can happen
+     *   when there is another border router publishing a NAT64 prefix with a higher priority.
+     * - `Active`: NAT64 prefix manager is enabled, running, and publishing a NAT64 prefix.
+     * @par
+     * When `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled, `Translator` returns one of the following states:
+     * - `Disabled`: NAT64 translator is disabled.
+     * - `NotRunning`: NAT64 translator is enabled, but is not translating packets. This could mean that the Translator
+     *   is not configured with a NAT64 prefix or a CIDR for NAT64.
+     * - `Active`: NAT64 translator is enabled and is translating packets.
+     * @sa otNat64GetPrefixManagerState
+     * @sa otNat64GetTranslatorState
      *
      */
-    else if (aArgs[0] == "rioprf")
+    else if (aArgs[0] == "state")
     {
-        if (aArgs[1].IsEmpty())
-        {
-            OutputLine("%s", PreferenceToString(otBorderRoutingGetRouteInfoOptionPreference(GetInstancePtr())));
-        }
-        else
-        {
-            otRoutePreference preference;
+        static const char *const kNat64State[] = {"Disabled", "NotRunning", "Idle", "Active"};
 
-            SuccessOrExit(error = ParsePreference(aArgs[1], preference));
-            otBorderRoutingSetRouteInfoOptionPreference(GetInstancePtr(), preference);
-        }
+        static_assert(0 == OT_NAT64_STATE_DISABLED, "OT_NAT64_STATE_DISABLED value is incorrect");
+        static_assert(1 == OT_NAT64_STATE_NOT_RUNNING, "OT_NAT64_STATE_NOT_RUNNING value is incorrect");
+        static_assert(2 == OT_NAT64_STATE_IDLE, "OT_NAT64_STATE_IDLE value is incorrect");
+        static_assert(3 == OT_NAT64_STATE_ACTIVE, "OT_NAT64_STATE_ACTIVE value is incorrect");
+
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+        OutputLine("PrefixManager: %s", kNat64State[otNat64GetPrefixManagerState(GetInstancePtr())]);
+#endif
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+        OutputLine("Translator: %s", kNat64State[otNat64GetTranslatorState(GetInstancePtr())]);
+#endif
     }
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
     /**
-     * @cli br prefixtable
+     * @cli nat64 cidr
      * @code
-     * br prefixtable
-     * prefix:fd00:1234:5678:0::/64, on-link:no, ms-since-rx:29526, lifetime:1800, route-prf:med,
-     * router:ff02:0:0:0:0:0:0:1
-     * prefix:1200:abba:baba:0::/64, on-link:yes, ms-since-rx:29527, lifetime:1800, preferred:1800,
-     * router:ff02:0:0:0:0:0:0:1
+     * nat64 cidr
+     * 192.168.255.0/24
      * Done
      * @endcode
      * @par api_copy
-     * #otBorderRoutingGetNextPrefixTableEntry
+     * #otNat64GetCidr
      *
      */
-    else if (aArgs[0] == "prefixtable")
+    else if (aArgs[0] == "cidr")
     {
-        otBorderRoutingPrefixTableIterator iterator;
-        otBorderRoutingPrefixTableEntry    entry;
+        otIp4Cidr cidr;
+        char      cidrString[OT_IP4_CIDR_STRING_SIZE];
 
-        otBorderRoutingPrefixTableInitIterator(GetInstancePtr(), &iterator);
+        SuccessOrExit(error = otNat64GetCidr(GetInstancePtr(), &cidr));
+        otIp4CidrToString(&cidr, cidrString, sizeof(cidrString));
+        OutputLine("%s", cidrString);
+    }
+    /**
+     * @cli nat64 mappings
+     * @code
+     * nat64 mappings
+     * |          | Address                   |        | 4 to 6       | 6 to 4       |
+     * +----------+---------------------------+--------+--------------+--------------+
+     * | ID       | IPv6       | IPv4         | Expiry | Pkts | Bytes | Pkts | Bytes |
+     * +----------+------------+--------------+--------+------+-------+------+-------+
+     * | 00021cb9 | fdc7::df79 | 192.168.64.2 |  7196s |    6 |   456 |   11 |  1928 |
+     * |          |                                TCP |    0 |     0 |    0 |     0 |
+     * |          |                                UDP |    1 |   136 |   16 |  1608 |
+     * |          |                               ICMP |    5 |   320 |    5 |   320 |
+     * @endcode
+     * @par api_copy
+     * #otNat64GetNextAddressMapping
+     *
+     */
+    else if (aArgs[0] == "mappings")
+    {
+        otNat64AddressMappingIterator iterator;
+        otNat64AddressMapping         mapping;
 
-        while (otBorderRoutingGetNextPrefixTableEntry(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
+        static const char *const kNat64StatusLevel1Title[] = {"", "Address", "", "4 to 6", "6 to 4"};
+
+        static const uint8_t kNat64StatusLevel1ColumnWidths[] = {
+            18, 61, 8, 25, 25,
+        };
+
+        static const char *const kNat64StatusTableHeader[] = {
+            "ID", "IPv6", "IPv4", "Expiry", "Pkts", "Bytes", "Pkts", "Bytes",
+        };
+
+        static const uint8_t kNat64StatusTableColumnWidths[] = {
+            18, 42, 18, 8, 10, 14, 10, 14,
+        };
+
+        OutputTableHeader(kNat64StatusLevel1Title, kNat64StatusLevel1ColumnWidths);
+        OutputTableHeader(kNat64StatusTableHeader, kNat64StatusTableColumnWidths);
+
+        otNat64InitAddressMappingIterator(GetInstancePtr(), &iterator);
+        while (otNat64GetNextAddressMapping(GetInstancePtr(), &iterator, &mapping) == OT_ERROR_NONE)
         {
-            char string[OT_IP6_PREFIX_STRING_SIZE];
+            char               ip4AddressString[OT_IP4_ADDRESS_STRING_SIZE];
+            char               ip6AddressString[OT_IP6_PREFIX_STRING_SIZE];
+            Uint64StringBuffer u64StringBuffer;
 
-            otIp6PrefixToString(&entry.mPrefix, string, sizeof(string));
-            OutputFormat("prefix:%s, on-link:%s, ms-since-rx:%u, lifetime:%u, ", string, entry.mIsOnLink ? "yes" : "no",
-                         entry.mMsecSinceLastUpdate, entry.mValidLifetime);
+            otIp6AddressToString(&mapping.mIp6, ip6AddressString, sizeof(ip6AddressString));
+            otIp4AddressToString(&mapping.mIp4, ip4AddressString, sizeof(ip4AddressString));
 
-            if (entry.mIsOnLink)
-            {
-                OutputFormat("preferred:%u, ", entry.mPreferredLifetime);
-            }
-            else
-            {
-                OutputFormat("route-prf:%s, ", PreferenceToString(entry.mRoutePreference));
-            }
+            OutputFormat("| %08lx%08lx ", ToUlong(static_cast<uint32_t>(mapping.mId >> 32)),
+                         ToUlong(static_cast<uint32_t>(mapping.mId & 0xffffffff)));
+            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));
 
-            otIp6AddressToString(&entry.mRouterAddress, string, sizeof(string));
-            OutputLine("router:%s", string);
+            OutputLine("|");
+
+            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("|");
+
+            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("|");
+
+            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("|");
         }
     }
+    /**
+     * @cli nat64 counters
+     * @code
+     * nat64 counters
+     * |               | 4 to 6                  | 6 to 4                  |
+     * +---------------+-------------------------+-------------------------+
+     * | Protocol      | Pkts     | Bytes        | Pkts     | Bytes        |
+     * +---------------+----------+--------------+----------+--------------+
+     * |         Total |       11 |          704 |       11 |          704 |
+     * |           TCP |        0 |            0 |        0 |            0 |
+     * |           UDP |        0 |            0 |        0 |            0 |
+     * |          ICMP |       11 |          704 |       11 |          704 |
+     * | Errors        | Pkts                    | Pkts                    |
+     * +---------------+-------------------------+-------------------------+
+     * |         Total |                       8 |                       4 |
+     * |   Illegal Pkt |                       0 |                       0 |
+     * |   Unsup Proto |                       0 |                       0 |
+     * |    No Mapping |                       2 |                       0 |
+     * Done
+     * @endcode
+     * @par
+     * Gets the NAT64 translator packet and error counters.
+     * @par
+     * Available when `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is enabled.
+     * @sa otNat64GetCounters
+     * @sa otNat64GetErrorCounters
+     *
+     */
+    else if (aArgs[0] == "counters")
+    {
+        static const char *const kNat64CounterTableHeader[] = {
+            "",
+            "4 to 6",
+            "6 to 4",
+        };
+        static const uint8_t     kNat64CounterTableHeaderColumns[] = {15, 25, 25};
+        static const char *const kNat64CounterTableSubHeader[]     = {
+                "Protocol", "Pkts", "Bytes", "Pkts", "Bytes",
+        };
+        static const uint8_t kNat64CounterTableSubHeaderColumns[] = {
+            15, 10, 14, 10, 14,
+        };
+        static const char *const kNat64CounterTableErrorSubHeader[] = {
+            "Errors",
+            "Pkts",
+            "Pkts",
+        };
+        static const uint8_t kNat64CounterTableErrorSubHeaderColumns[] = {
+            15,
+            25,
+            25,
+        };
+        static const char *const kNat64CounterErrorType[] = {
+            "Unknown",
+            "Illegal Pkt",
+            "Unsup Proto",
+            "No Mapping",
+        };
+
+        otNat64ProtocolCounters counters;
+        otNat64ErrorCounters    errorCounters;
+        Uint64StringBuffer      u64StringBuffer;
+
+        OutputTableHeader(kNat64CounterTableHeader, kNat64CounterTableHeaderColumns);
+        OutputTableHeader(kNat64CounterTableSubHeader, kNat64CounterTableSubHeaderColumns);
+
+        otNat64GetCounters(GetInstancePtr(), &counters);
+        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));
+
+        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));
+
+        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));
+
+        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));
+
+        OutputTableHeader(kNat64CounterTableErrorSubHeader, kNat64CounterTableErrorSubHeaderColumns);
+        for (uint8_t i = 0; i < OT_NAT64_DROP_REASON_COUNT; i++)
+        {
+            OutputFormat("| %13s | %23s ", kNat64CounterErrorType[i],
+                         Uint64ToString(errorCounters.mCount4To6[i], u64StringBuffer));
+            OutputLine("| %23s |", Uint64ToString(errorCounters.mCount6To4[i], u64StringBuffer));
+        }
+    }
+#endif // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
     else
     {
-        error = OT_ERROR_INVALID_COMMAND;
+        ExitNow(error = OT_ERROR_INVALID_COMMAND);
     }
 
 exit:
     return error;
 }
-#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#endif // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE || OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
 
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
 template <> otError Interpreter::Process<Cmd("bbr")>(Arg aArgs[])
@@ -692,15 +860,34 @@
     otError                error = OT_ERROR_INVALID_COMMAND;
     otBackboneRouterConfig config;
 
+    /**
+     * @cli bbr
+     * @code
+     * bbr
+     * BBR Primary:
+     * server16: 0xE400
+     * seqno:    10
+     * delay:    120 secs
+     * timeout:  300 secs
+     * Done
+     * @endcode
+     * @code
+     * bbr
+     * BBR Primary: None
+     * Done
+     * @endcode
+     * @par
+     * Returns the current Primary Backbone Router information for the Thread device.
+     */
     if (aArgs[0].IsEmpty())
     {
         if (otBackboneRouterGetPrimary(GetInstancePtr(), &config) == OT_ERROR_NONE)
         {
             OutputLine("BBR Primary:");
             OutputLine("server16: 0x%04X", config.mServer16);
-            OutputLine("seqno:    %d", config.mSequenceNumber);
-            OutputLine("delay:    %d secs", config.mReregistrationDelay);
-            OutputLine("timeout:  %d secs", config.mMlrTimeout);
+            OutputLine("seqno:    %u", config.mSequenceNumber);
+            OutputLine("delay:    %u secs", config.mReregistrationDelay);
+            OutputLine("timeout:  %lu secs", ToUlong(config.mMlrTimeout));
         }
         else
         {
@@ -719,6 +906,34 @@
                 ExitNow(error = OT_ERROR_INVALID_COMMAND);
             }
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE && OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+            /**
+             * @cli bbr mgmt dua
+             * @code
+             * bbr mgmt dua 1 2f7c235e5025a2fd
+             * Done
+             * @endcode
+             * @code
+             * bbr mgmt dua 160
+             * Done
+             * @endcode
+             * @cparam bbr mgmt dua @ca{status|coap-code} [@ca{meshLocalIid}]
+             * For `status` or `coap-code`, use:
+             * *    0: ST_DUA_SUCCESS
+             * *    1: ST_DUA_REREGISTER
+             * *    2: ST_DUA_INVALID
+             * *    3: ST_DUA_DUPLICATE
+             * *    4: ST_DUA_NO_RESOURCES
+             * *    5: ST_DUA_BBR_NOT_PRIMARY
+             * *    6: ST_DUA_GENERAL_FAILURE
+             * *    160: COAP code 5.00
+             * @par
+             * With the `meshLocalIid` included, this command configures the response status
+             * for the next DUA registration. Without `meshLocalIid`, respond to the next
+             * DUA.req with the specified `status` or `coap-code`.
+             * @par
+             * Available when `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is enabled.
+             * @sa otBackboneRouterConfigNextDuaRegistrationResponse
+             */
             else if (aArgs[1] == "dua")
             {
                 uint8_t                   status;
@@ -761,6 +976,22 @@
 {
     otError error = OT_ERROR_INVALID_COMMAND;
 
+    /**
+     * @cli bbr mgmt mlr listener
+     * @code
+     * bbr mgmt mlr listener
+     * ff04:0:0:0:0:0:0:abcd 3534000
+     * ff04:0:0:0:0:0:0:eeee 3537610
+     * Done
+     * @endcode
+     * @par
+     * Returns the Multicast Listeners with the #otBackboneRouterMulticastListenerInfo
+     * `mTimeout` in seconds.
+     * @par
+     * Available when `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` and
+     * `OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE` are enabled.
+     * @sa otBackboneRouterMulticastListenerGetNext
+     */
     if (aArgs[0] == "listener")
     {
         if (aArgs[1].IsEmpty())
@@ -770,11 +1001,34 @@
         }
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+        /**
+         * @cli bbr mgmt mlr listener clear
+         * @code
+         * bbr mgmt mlr listener clear
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otBackboneRouterMulticastListenerClear
+         */
         if (aArgs[1] == "clear")
         {
             otBackboneRouterMulticastListenerClear(GetInstancePtr());
             error = OT_ERROR_NONE;
         }
+        /**
+         * @cli bbr mgmt mlr listener add
+         * @code
+         * bbr mgmt mlr listener add ff04::1
+         * Done
+         * @endcode
+         * @code
+         * bbr mgmt mlr listener add ff04::2 300
+         * Done
+         * @endcode
+         * @cparam bbr mgmt mlr listener add @ca{ipaddress} [@ca{timeout-seconds}]
+         * @par api_copy
+         * #otBackboneRouterMulticastListenerAdd
+         */
         else if (aArgs[1] == "add")
         {
             otIp6Address address;
@@ -791,6 +1045,23 @@
             error = otBackboneRouterMulticastListenerAdd(GetInstancePtr(), &address, timeout);
         }
     }
+    /**
+     * @cli bbr mgmt mlr response
+     * @code
+     * bbr mgmt mlr response 2
+     * Done
+     * @endcode
+     * @cparam bbr mgmt mlr response @ca{status-code}
+     * For `status-code`, use:
+     * *    0: ST_MLR_SUCCESS
+     * *    2: ST_MLR_INVALID
+     * *    3: ST_MLR_NO_PERSISTENT
+     * *    4: ST_MLR_NO_RESOURCES
+     * *    5: ST_MLR_BBR_NOT_PRIMARY
+     * *    6: ST_MLR_GENERAL_FAILURE
+     * @par api_copy
+     * #otBackboneRouterConfigNextMulticastListenerRegistrationResponse
+     */
     else if (aArgs[0] == "response")
     {
         error = ProcessSet(aArgs + 1, otBackboneRouterConfigNextMulticastListenerRegistrationResponse);
@@ -809,7 +1080,7 @@
     while (otBackboneRouterMulticastListenerGetNext(GetInstancePtr(), &iter, &listenerInfo) == OT_ERROR_NONE)
     {
         OutputIp6Address(listenerInfo.mAddress);
-        OutputLine(" %u", listenerInfo.mTimeout);
+        OutputLine(" %lu", ToUlong(listenerInfo.mTimeout));
     }
 }
 
@@ -821,18 +1092,81 @@
     otBackboneRouterConfig config;
     bool                   enable;
 
+    /**
+     * @cli bbr (enable,disable)
+     * @code
+     * bbr enable
+     * Done
+     * @endcode
+     * @code
+     * bbr disable
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBackboneRouterSetEnabled
+     */
     if (ParseEnableOrDisable(aArgs[0], enable) == OT_ERROR_NONE)
     {
         otBackboneRouterSetEnabled(GetInstancePtr(), enable);
     }
+    /**
+     * @cli bbr jitter (get,set)
+     * @code
+     * bbr jitter
+     * 20
+     * Done
+     * @endcode
+     * @code
+     * bbr jitter 10
+     * Done
+     * @endcode
+     * @cparam bbr jitter [@ca{jitter}]
+     * @par
+     * Gets or sets jitter (in seconds) for Backbone Router registration.
+     * @par
+     * Available when `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` is enabled.
+     * @sa otBackboneRouterGetRegistrationJitter
+     * @sa otBackboneRouterSetRegistrationJitter
+     */
     else if (aArgs[0] == "jitter")
     {
         error = ProcessGetSet(aArgs + 1, otBackboneRouterGetRegistrationJitter, otBackboneRouterSetRegistrationJitter);
     }
+    /**
+     * @cli bbr register
+     * @code
+     * bbr register
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBackboneRouterRegister
+     */
     else if (aArgs[0] == "register")
     {
         SuccessOrExit(error = otBackboneRouterRegister(GetInstancePtr()));
     }
+    /**
+     * @cli bbr state
+     * @code
+     * bbr state
+     * Disabled
+     * Done
+     * @endcode
+     * @code
+     * bbr state
+     * Primary
+     * Done
+     * @endcode
+     * @code
+     * bbr state
+     * Secondary
+     * Done
+     * @endcode
+     * @par
+     * Available when `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` is enabled.
+     * @par api_copy
+     * #otBackboneRouterGetState
+     */
     else if (aArgs[0] == "state")
     {
         static const char *const kStateStrings[] = {
@@ -847,19 +1181,44 @@
 
         OutputLine("%s", Stringify(otBackboneRouterGetState(GetInstancePtr()), kStateStrings));
     }
+    /**
+     * @cli bbr config
+     * @code
+     * bbr config
+     * seqno:    10
+     * delay:    120 secs
+     * timeout:  300 secs
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBackboneRouterGetConfig
+     */
     else if (aArgs[0] == "config")
     {
         otBackboneRouterGetConfig(GetInstancePtr(), &config);
 
         if (aArgs[1].IsEmpty())
         {
-            OutputLine("seqno:    %d", config.mSequenceNumber);
-            OutputLine("delay:    %d secs", config.mReregistrationDelay);
-            OutputLine("timeout:  %d secs", config.mMlrTimeout);
+            OutputLine("seqno:    %u", config.mSequenceNumber);
+            OutputLine("delay:    %u secs", config.mReregistrationDelay);
+            OutputLine("timeout:  %lu secs", ToUlong(config.mMlrTimeout));
         }
         else
         {
             // Set local Backbone Router configuration.
+            /**
+             * @cli bbr config (set)
+             * @code
+             * bbr config seqno 20 delay 30
+             * Done
+             * @endcode
+             * @cparam bbr config [seqno @ca{seqno}] [delay @ca{delay}] [timeout @ca{timeout}]
+             * @par
+             * `bbr register` should be issued explicitly to register Backbone Router service to Leader
+             * for Secondary Backbone Router.
+             * @par api_copy
+             * #otBackboneRouterSetConfig
+             */
             for (Arg *arg = &aArgs[1]; !arg->IsEmpty(); arg++)
             {
                 if (*arg == "seqno")
@@ -896,21 +1255,30 @@
 }
 #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;
-
-    if (aArgs[0].IsEmpty())
-    {
-        OutputLine("%s", otThreadGetDomainName(GetInstancePtr()));
-    }
-    else
-    {
-        SuccessOrExit(error = otThreadSetDomainName(GetInstancePtr(), aArgs[0].GetCString()));
-    }
-
-exit:
-    return error;
+    /**
+     * @cli domainname (set)
+     * @code
+     * domainname Test\ Thread
+     * Done
+     * @endcode
+     * @cparam domainname @ca{name}
+     * Use a `backslash` to escape spaces.
+     * @par api_copy
+     * #otThreadSetDomainName
+     */
+    return ProcessGetSet(aArgs, otThreadGetDomainName, otThreadSetDomainName);
 }
 
 #if OPENTHREAD_CONFIG_DUA_ENABLE
@@ -918,6 +1286,16 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli dua iid
+     * @code
+     * dua iid
+     * 0004000300020001
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetFixedDuaInterfaceIdentifier
+     */
     if (aArgs[0] == "iid")
     {
         if (aArgs[1].IsEmpty())
@@ -929,6 +1307,22 @@
                 OutputBytesLine(iid->mFields.m8);
             }
         }
+        /**
+         * @cli dua iid (set,clear)
+         * @code
+         * dua iid 0004000300020001
+         * Done
+         * @endcode
+         * @code
+         * dua iid clear
+         * Done
+         * @endcode
+         * @cparam dua iid @ca{iid|clear}
+         * `dua iid clear` passes a `nullptr` to #otThreadSetFixedDuaInterfaceIdentifier.
+         * Otherwise, you can pass the `iid`.
+         * @par api_copy
+         * #otThreadSetFixedDuaInterfaceIdentifier
+         */
         else if (aArgs[1] == "clear")
         {
             error = otThreadSetFixedDuaInterfaceIdentifier(GetInstancePtr(), nullptr);
@@ -953,14 +1347,43 @@
 
 #endif // (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
 
+/**
+ * @cli bufferinfo
+ * @code
+ * bufferinfo
+ * total: 40
+ * free: 40
+ * max-used: 5
+ * 6lo send: 0 0 0
+ * 6lo reas: 0 0 0
+ * ip6: 0 0 0
+ * mpl: 0 0 0
+ * mle: 0 0 0
+ * coap: 0 0 0
+ * coap secure: 0 0 0
+ * application coap: 0 0 0
+ * Done
+ * @endcode
+ * @par
+ * Gets the current message buffer information.
+ * *   `total` displays the total number of message buffers in pool.
+ * *   `free` displays the number of free message buffers.
+ * *   `max-used` displays max number of used buffers at the same time since OT stack
+ *     initialization or last `bufferinfo reset`.
+ * @par
+ * Next, the CLI displays info about different queues used by the OpenThread stack,
+ * for example `6lo send`. Each line after the queue represents info about a queue:
+ * *   The first number shows number messages in the queue.
+ * *   The second number shows number of buffers used by all messages in the queue.
+ * *   The third number shows total number of bytes of all messages in the queue.
+ * @sa otMessageGetBufferInfo
+ */
 template <> otError Interpreter::Process<Cmd("bufferinfo")>(Arg aArgs[])
 {
-    OT_UNUSED_VARIABLE(aArgs);
-
     struct BufferInfoName
     {
         const otMessageQueueInfo otBufferInfo::*mQueuePtr;
-        const char *                            mName;
+        const char                             *mName;
     };
 
     static const BufferInfoName kBufferInfoNames[] = {
@@ -974,22 +1397,64 @@
         {&otBufferInfo::mApplicationCoapQueue, "application coap"},
     };
 
-    otBufferInfo bufferInfo;
+    otError error = OT_ERROR_NONE;
 
-    otMessageGetBufferInfo(GetInstancePtr(), &bufferInfo);
-
-    OutputLine("total: %d", bufferInfo.mTotalBuffers);
-    OutputLine("free: %d", bufferInfo.mFreeBuffers);
-
-    for (const BufferInfoName &info : kBufferInfoNames)
+    if (aArgs[0].IsEmpty())
     {
-        OutputLine("%s: %u %u %u", info.mName, (bufferInfo.*info.mQueuePtr).mNumMessages,
-                   (bufferInfo.*info.mQueuePtr).mNumBuffers, (bufferInfo.*info.mQueuePtr).mTotalBytes);
+        otBufferInfo bufferInfo;
+
+        otMessageGetBufferInfo(GetInstancePtr(), &bufferInfo);
+
+        OutputLine("total: %u", bufferInfo.mTotalBuffers);
+        OutputLine("free: %u", bufferInfo.mFreeBuffers);
+        OutputLine("max-used: %u", bufferInfo.mMaxUsedBuffers);
+
+        for (const BufferInfoName &info : kBufferInfoNames)
+        {
+            OutputLine("%s: %u %u %lu", info.mName, (bufferInfo.*info.mQueuePtr).mNumMessages,
+                       (bufferInfo.*info.mQueuePtr).mNumBuffers, ToUlong((bufferInfo.*info.mQueuePtr).mTotalBytes));
+        }
+    }
+    /**
+     * @cli bufferinfo reset
+     * @code
+     * bufferinfo reset
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otMessageResetBufferInfo
+     */
+    else if (aArgs[0] == "reset")
+    {
+        otMessageResetBufferInfo(GetInstancePtr());
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_ARGS;
     }
 
-    return OT_ERROR_NONE;
+    return error;
 }
 
+/**
+ * @cli ccathreshold (get,set)
+ * @code
+ * ccathreshold
+ * -75 dBm
+ * Done
+ * @endcode
+ * @code
+ * ccathreshold -62
+ * Done
+ * @endcode
+ * @cparam ccathreshold [@ca{CCA-threshold-dBm}]
+ * Use the optional `CCA-threshold-dBm` argument to set the CCA threshold.
+ * @par
+ * Gets or sets the CCA threshold in dBm measured at the antenna connector per
+ * IEEE 802.15.4 - 2015 section 10.1.4.
+ * @sa otPlatRadioGetCcaEnergyDetectThreshold
+ * @sa otPlatRadioSetCcaEnergyDetectThreshold
+ */
 template <> otError Interpreter::Process<Cmd("ccathreshold")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
@@ -1041,19 +1506,87 @@
 
 #endif
 
+/**
+ * @cli channel (get,set)
+ * @code
+ * channel
+ * 11
+ * Done
+ * @endcode
+ * @code
+ * channel 11
+ * Done
+ * @endcode
+ * @cparam channel [@ca{channel-num}]
+ * Use `channel-num` to set the channel.
+ * @par
+ * Gets or sets the IEEE 802.15.4 Channel value.
+ */
 template <> otError Interpreter::Process<Cmd("channel")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli channel supported
+     * @code
+     * channel supported
+     * 0x7fff800
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otPlatRadioGetSupportedChannelMask
+     */
     if (aArgs[0] == "supported")
     {
-        OutputLine("0x%x", otPlatRadioGetSupportedChannelMask(GetInstancePtr()));
+        OutputLine("0x%lx", ToUlong(otPlatRadioGetSupportedChannelMask(GetInstancePtr())));
     }
+    /**
+     * @cli channel preferred
+     * @code
+     * channel preferred
+     * 0x7fff800
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otPlatRadioGetPreferredChannelMask
+     */
     else if (aArgs[0] == "preferred")
     {
-        OutputLine("0x%x", otPlatRadioGetPreferredChannelMask(GetInstancePtr()));
+        OutputLine("0x%lx", ToUlong(otPlatRadioGetPreferredChannelMask(GetInstancePtr())));
     }
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
+    /**
+     * @cli channel monitor
+     * @code
+     * channel monitor
+     * enabled: 1
+     * interval: 41000
+     * threshold: -75
+     * window: 960
+     * count: 10552
+     * occupancies:
+     * ch 11 (0x0cb7)  4.96% busy
+     * ch 12 (0x2e2b) 18.03% busy
+     * ch 13 (0x2f54) 18.48% busy
+     * ch 14 (0x0fef)  6.22% busy
+     * ch 15 (0x1536)  8.28% busy
+     * ch 16 (0x1746)  9.09% busy
+     * ch 17 (0x0b8b)  4.50% busy
+     * ch 18 (0x60a7) 37.75% busy
+     * ch 19 (0x0810)  3.14% busy
+     * ch 20 (0x0c2a)  4.75% busy
+     * ch 21 (0x08dc)  3.46% busy
+     * ch 22 (0x101d)  6.29% busy
+     * ch 23 (0x0092)  0.22% busy
+     * ch 24 (0x0028)  0.06% busy
+     * ch 25 (0x0063)  0.15% busy
+     * ch 26 (0x058c)  2.16% busy
+     * Done
+     * @endcode
+     * @par
+     * Get the current channel monitor state and channel occupancy.
+     * `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` is required.
+     */
     else if (aArgs[0] == "monitor")
     {
         if (aArgs[1].IsEmpty())
@@ -1064,15 +1597,17 @@
                 uint32_t channelMask = otLinkGetSupportedChannelMask(GetInstancePtr());
                 uint8_t  channelNum  = sizeof(channelMask) * CHAR_BIT;
 
-                OutputLine("interval: %u", otChannelMonitorGetSampleInterval(GetInstancePtr()));
+                OutputLine("interval: %lu", ToUlong(otChannelMonitorGetSampleInterval(GetInstancePtr())));
                 OutputLine("threshold: %d", otChannelMonitorGetRssiThreshold(GetInstancePtr()));
-                OutputLine("window: %u", otChannelMonitorGetSampleWindow(GetInstancePtr()));
-                OutputLine("count: %u", otChannelMonitorGetSampleCount(GetInstancePtr()));
+                OutputLine("window: %lu", ToUlong(otChannelMonitorGetSampleWindow(GetInstancePtr())));
+                OutputLine("count: %lu", ToUlong(otChannelMonitorGetSampleCount(GetInstancePtr())));
 
                 OutputLine("occupancies:");
+
                 for (uint8_t channel = 0; channel < channelNum; channel++)
                 {
-                    uint32_t occupancy = 0;
+                    uint16_t               occupancy;
+                    PercentageStringBuffer stringBuffer;
 
                     if (!((1UL << channel) & channelMask))
                     {
@@ -1081,17 +1616,43 @@
 
                     occupancy = otChannelMonitorGetChannelOccupancy(GetInstancePtr(), channel);
 
-                    OutputFormat("ch %d (0x%04x) ", channel, occupancy);
-                    occupancy = (occupancy * 10000) / 0xffff;
-                    OutputLine("%2d.%02d%% busy", occupancy / 100, occupancy % 100);
+                    OutputLine("ch %u (0x%04x) %6s%% busy", channel, occupancy,
+                               PercentageToString(occupancy, stringBuffer));
                 }
-                OutputLine("");
+
+                OutputNewLine();
             }
         }
+        /**
+         * @cli channel monitor start
+         * @code
+         * channel monitor start
+         * channel monitor start
+         * Done
+         * @endcode
+         * @par
+         * Start the channel monitor.
+         * OT CLI sends a boolean value of `true` to #otChannelMonitorSetEnabled.
+         * `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` is required.
+         * @sa otChannelMonitorSetEnabled
+         */
         else if (aArgs[1] == "start")
         {
             error = otChannelMonitorSetEnabled(GetInstancePtr(), true);
         }
+        /**
+         * @cli channel monitor stop
+         * @code
+         * channel monitor stop
+         * channel monitor stop
+         * Done
+         * @endcode
+         * @par
+         * Stop the channel monitor.
+         * OT CLI sends a boolean value of `false` to #otChannelMonitorSetEnabled.
+         * `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` is required.
+         * @sa otChannelMonitorSetEnabled
+         */
         else if (aArgs[1] == "stop")
         {
             error = otChannelMonitorSetEnabled(GetInstancePtr(), false);
@@ -1105,9 +1666,26 @@
 #if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
     else if (aArgs[0] == "manager")
     {
+        /**
+         * @cli channel manager
+         * @code
+         * channel manager
+         * channel: 11
+         * auto: 1
+         * delay: 120
+         * interval: 10800
+         * supported: { 11-26}
+         * favored: { 11-26}
+         * Done
+         * @endcode
+         * @par
+         * Get the channel manager state.
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` is required.
+         * @sa otChannelManagerGetRequestedChannel
+         */
         if (aArgs[1].IsEmpty())
         {
-            OutputLine("channel: %d", otChannelManagerGetRequestedChannel(GetInstancePtr()));
+            OutputLine("channel: %u", otChannelManagerGetRequestedChannel(GetInstancePtr()));
             OutputLine("auto: %d", otChannelManagerGetAutoChannelSelectionEnabled(GetInstancePtr()));
 
             if (otChannelManagerGetAutoChannelSelectionEnabled(GetInstancePtr()))
@@ -1115,18 +1693,45 @@
                 Mac::ChannelMask supportedMask(otChannelManagerGetSupportedChannels(GetInstancePtr()));
                 Mac::ChannelMask favoredMask(otChannelManagerGetFavoredChannels(GetInstancePtr()));
 
-                OutputLine("delay: %d", otChannelManagerGetDelay(GetInstancePtr()));
-                OutputLine("interval: %u", otChannelManagerGetAutoChannelSelectionInterval(GetInstancePtr()));
+                OutputLine("delay: %u", otChannelManagerGetDelay(GetInstancePtr()));
+                OutputLine("interval: %lu", ToUlong(otChannelManagerGetAutoChannelSelectionInterval(GetInstancePtr())));
                 OutputLine("cca threshold: 0x%04x", otChannelManagerGetCcaFailureRateThreshold(GetInstancePtr()));
                 OutputLine("supported: %s", supportedMask.ToString().AsCString());
-                OutputLine("favored: %s", supportedMask.ToString().AsCString());
+                OutputLine("favored: %s", favoredMask.ToString().AsCString());
             }
         }
+        /**
+         * @cli channel manager change
+         * @code
+         * channel manager change 11
+         * channel manager change 11
+         * Done
+         * @endcode
+         * @cparam channel manager change @ca{channel-num}
+         * @par
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` is required.
+         * @par api_copy
+         * #otChannelManagerRequestChannelChange
+         */
         else if (aArgs[1] == "change")
         {
             error = ProcessSet(aArgs + 2, otChannelManagerRequestChannelChange);
         }
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
+        /**
+         * @cli channel manager select
+         * @code
+         * channel manager select 1
+         * channel manager select 1
+         * Done
+         * @endcode
+         * @cparam channel manager select @ca{skip-quality-check}
+         * Use a `1` or `0` for the boolean `skip-quality-check`.
+         * @par
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` are required.
+         * @par api_copy
+         * #otChannelManagerRequestChannelSelect
+         */
         else if (aArgs[1] == "select")
         {
             bool enable;
@@ -1135,6 +1740,20 @@
             error = otChannelManagerRequestChannelSelect(GetInstancePtr(), enable);
         }
 #endif
+        /**
+         * @cli channel manager auto
+         * @code
+         * channel manager auto 1
+         * channel manager auto 1
+         * Done
+         * @endcode
+         * @cparam channel manager auto @ca{enable}
+         * `1` is a boolean to `enable`.
+         * @par
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` are required.
+         * @par api_copy
+         * #otChannelManagerSetAutoChannelSelectionEnabled
+         */
         else if (aArgs[1] == "auto")
         {
             bool enable;
@@ -1142,22 +1761,88 @@
             SuccessOrExit(error = aArgs[2].ParseAsBool(enable));
             otChannelManagerSetAutoChannelSelectionEnabled(GetInstancePtr(), enable);
         }
+        /**
+         * @cli channel manager delay
+         * @code
+         * channel manager delay 120
+         * channel manager delay 120
+         * Done
+         * @endcode
+         * @cparam channel manager delay @ca{delay-seconds}
+         * @par
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` are required.
+         * @par api_copy
+         * #otChannelManagerSetDelay
+         */
         else if (aArgs[1] == "delay")
         {
-            error = ProcessSet(aArgs + 2, otChannelManagerSetDelay);
+            error = ProcessGetSet(aArgs + 2, otChannelManagerGetDelay, otChannelManagerSetDelay);
         }
+        /**
+         * @cli channel manager interval
+         * @code
+         * channel manager interval 10800
+         * channel manager interval 10800
+         * Done
+         * @endcode
+         * @cparam channel manager interval @ca{interval-seconds}
+         * @par
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` are required.
+         * @par api_copy
+         * #otChannelManagerSetAutoChannelSelectionInterval
+         */
         else if (aArgs[1] == "interval")
         {
             error = ProcessSet(aArgs + 2, otChannelManagerSetAutoChannelSelectionInterval);
         }
+        /**
+         * @cli channel manager supported
+         * @code
+         * channel manager supported 0x7fffc00
+         * channel manager supported 0x7fffc00
+         * Done
+         * @endcode
+         * @cparam channel manager supported @ca{mask}
+         * @par
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` are required.
+         * @par api_copy
+         * #otChannelManagerSetSupportedChannels
+         */
         else if (aArgs[1] == "supported")
         {
             error = ProcessSet(aArgs + 2, otChannelManagerSetSupportedChannels);
         }
+        /**
+         * @cli channel manager favored
+         * @code
+         * channel manager favored 0x7fffc00
+         * channel manager favored 0x7fffc00
+         * Done
+         * @endcode
+         * @cparam channel manager favored @ca{mask}
+         * @par
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` are required.
+         * @par api_copy
+         * #otChannelManagerSetFavoredChannels
+         */
         else if (aArgs[1] == "favored")
         {
             error = ProcessSet(aArgs + 2, otChannelManagerSetFavoredChannels);
         }
+        /**
+         * @cli channel manager threshold
+         * @code
+         * channel manager threshold 0xffff
+         * channel manager threshold 0xffff
+         * Done
+         * @endcode
+         * @cparam channel manager threshold @ca{threshold-percent}
+         * Use a hex value for `threshold-percent`. `0` maps to 0% and `0xffff` maps to 100%.
+         * @par
+         * `OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE` and `OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE` are required.
+         * @par api_copy
+         * #otChannelManagerSetCcaFailureRateThreshold
+         */
         else if (aArgs[1] == "threshold")
         {
             error = ProcessSet(aArgs + 2, otChannelManagerSetCcaFailureRateThreshold);
@@ -1193,15 +1878,29 @@
     {
         uint16_t maxChildren;
 
+        /**
+         * @cli child table
+         * @code
+         * child table
+         * | ID  | RLOC16 | Timeout    | Age        | LQ In | C_VN |R|D|N|Ver|CSL|QMsgCnt| Extended MAC     |
+         * +-----+--------+------------+------------+-------+------+-+-+-+---+---+-------+------------------+
+         * |   1 | 0xc801 |        240 |         24 |     3 |  131 |1|0|0|  3| 0 |     0 | 4ecede68435358ac |
+         * |   2 | 0xc802 |        240 |          2 |     3 |  131 |0|0|0|  3| 1 |     0 | a672a601d2ce37d8 |
+         * Done
+         * @endcode
+         * @par
+         * Prints a table of the attached children.
+         * @sa otThreadGetChildInfoByIndex
+         */
         if (isTable)
         {
             static const char *const kChildTableTitles[] = {
-                "ID", "RLOC16", "Timeout", "Age", "LQ In",   "C_VN",         "R",
-                "D",  "N",      "Ver",     "CSL", "QMsgCnt", "Extended MAC",
+                "ID", "RLOC16", "Timeout", "Age", "LQ In",   "C_VN",    "R",
+                "D",  "N",      "Ver",     "CSL", "QMsgCnt", "Suprvsn", "Extended MAC",
             };
 
             static const uint8_t kChildTableColumnWidths[] = {
-                5, 8, 12, 12, 7, 6, 1, 1, 1, 3, 3, 7, 18,
+                5, 8, 12, 12, 7, 6, 1, 1, 1, 3, 3, 7, 7, 18,
             };
 
             OutputTableHeader(kChildTableTitles, kChildTableColumnWidths);
@@ -1219,36 +1918,68 @@
 
             if (isTable)
             {
-                OutputFormat("| %3d ", childInfo.mChildId);
+                OutputFormat("| %3u ", childInfo.mChildId);
                 OutputFormat("| 0x%04x ", childInfo.mRloc16);
-                OutputFormat("| %10d ", childInfo.mTimeout);
-                OutputFormat("| %10d ", childInfo.mAge);
-                OutputFormat("| %5d ", childInfo.mLinkQualityIn);
-                OutputFormat("| %4d ", childInfo.mNetworkDataVersion);
+                OutputFormat("| %10lu ", ToUlong(childInfo.mTimeout));
+                OutputFormat("| %10lu ", ToUlong(childInfo.mAge));
+                OutputFormat("| %5u ", childInfo.mLinkQualityIn);
+                OutputFormat("| %4u ", childInfo.mNetworkDataVersion);
                 OutputFormat("|%1d", childInfo.mRxOnWhenIdle);
                 OutputFormat("|%1d", childInfo.mFullThreadDevice);
                 OutputFormat("|%1d", childInfo.mFullNetworkData);
-                OutputFormat("|%3d", childInfo.mVersion);
+                OutputFormat("|%3u", childInfo.mVersion);
                 OutputFormat("| %1d ", childInfo.mIsCslSynced);
-                OutputFormat("| %5d ", childInfo.mQueuedMessageCnt);
+                OutputFormat("| %5u ", childInfo.mQueuedMessageCnt);
+                OutputFormat("| %5u ", childInfo.mSupervisionInterval);
                 OutputFormat("| ");
                 OutputExtAddress(childInfo.mExtAddress);
                 OutputLine(" |");
             }
+            /**
+             * @cli child list
+             * @code
+             * child list
+             * 1 2 3 6 7 8
+             * Done
+             * @endcode
+             * @par
+             * Returns a list of attached Child IDs.
+             * @sa otThreadGetChildInfoByIndex
+             */
             else
             {
-                OutputFormat("%d ", childInfo.mChildId);
+                OutputFormat("%u ", childInfo.mChildId);
             }
         }
 
-        OutputLine("");
+        OutputNewLine();
         ExitNow();
     }
 
     SuccessOrExit(error = aArgs[0].ParseAsUint16(childId));
     SuccessOrExit(error = otThreadGetChildInfoById(GetInstancePtr(), childId, &childInfo));
 
-    OutputLine("Child ID: %d", childInfo.mChildId);
+    /**
+     * @cli child (id)
+     * @code
+     * child 1
+     * Child ID: 1
+     * Rloc: 9c01
+     * Ext Addr: e2b3540590b0fd87
+     * Mode: rn
+     * CSL Synchronized: 1
+     * Net Data: 184
+     * Timeout: 100
+     * Age: 0
+     * Link Quality In: 3
+     * RSSI: -20
+     * Done
+     * @endcode
+     * @cparam child @ca{child-id}
+     * @par api_copy
+     * #otThreadGetChildInfoById
+     */
+    OutputLine("Child ID: %u", childInfo.mChildId);
     OutputLine("Rloc: %04x", childInfo.mRloc16);
     OutputFormat("Ext Addr: ");
     OutputExtAddressLine(childInfo.mExtAddress);
@@ -1256,11 +1987,13 @@
     linkMode.mDeviceType   = childInfo.mFullThreadDevice;
     linkMode.mNetworkData  = childInfo.mFullThreadDevice;
     OutputLine("Mode: %s", LinkModeToString(linkMode, linkModeString));
-    OutputLine("Net Data: %d", childInfo.mNetworkDataVersion);
-    OutputLine("Timeout: %d", childInfo.mTimeout);
-    OutputLine("Age: %d", childInfo.mAge);
-    OutputLine("Link Quality In: %d", childInfo.mLinkQualityIn);
+    OutputLine("CSL Synchronized: %d ", childInfo.mIsCslSynced);
+    OutputLine("Net Data: %u", childInfo.mNetworkDataVersion);
+    OutputLine("Timeout: %lu", ToUlong(childInfo.mTimeout));
+    OutputLine("Age: %lu", ToUlong(childInfo.mAge));
+    OutputLine("Link Quality In: %u", childInfo.mLinkQualityIn);
     OutputLine("RSSI: %d", childInfo.mAverageRssi);
+    OutputLine("Supervision Interval: %d", childInfo.mSupervisionInterval);
 
 exit:
     return error;
@@ -1270,6 +2003,17 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli childip
+     * @code
+     * childip
+     * 3401: fdde:ad00:beef:0:3037:3e03:8c5f:bc0c
+     * Done
+     * @endcode
+     * @par
+     * Gets a list of IP addresses stored for MTD children.
+     * @sa otThreadGetChildNextIp6Address
+     */
     if (aArgs[0].IsEmpty())
     {
         uint16_t maxChildren = otThreadGetMaxAllowedChildren(GetInstancePtr());
@@ -1296,11 +2040,31 @@
             }
         }
     }
+    /**
+     * @cli childip max
+     * @code
+     * childip max
+     * 4
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetMaxChildIpAddresses
+     */
     else if (aArgs[0] == "max")
     {
 #if !OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
         error = ProcessGet(aArgs + 1, otThreadGetMaxChildIpAddresses);
 #else
+        /**
+         * @cli childip max (set)
+         * @code
+         * childip max 2
+         * Done
+         * @endcode
+         * @cparam childip max @ca{count}
+         * @par api_copy
+         * #otThreadSetMaxChildIpAddresses
+         */
         error = ProcessGetSet(aArgs + 1, otThreadGetMaxChildIpAddresses, otThreadSetMaxChildIpAddresses);
 #endif
     }
@@ -1312,49 +2076,129 @@
     return error;
 }
 
+/**
+ * @cli childmax
+ * @code
+ * childmax
+ * 5
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otThreadGetMaxAllowedChildren
+ */
 template <> otError Interpreter::Process<Cmd("childmax")>(Arg aArgs[])
 {
+    /**
+     * @cli childmax (set)
+     * @code
+     * childmax 2
+     * Done
+     * @endcode
+     * @cparam childmax @ca{count}
+     * @par api_copy
+     * #otThreadSetMaxAllowedChildren
+     */
     return ProcessGetSet(aArgs, otThreadGetMaxAllowedChildren, otThreadSetMaxAllowedChildren);
 }
 #endif // OPENTHREAD_FTD
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
 template <> otError Interpreter::Process<Cmd("childsupervision")>(Arg aArgs[])
 {
     otError error = OT_ERROR_INVALID_ARGS;
 
+    /**
+     * @cli childsupervision checktimeout
+     * @code
+     * childsupervision checktimeout
+     * 30
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otChildSupervisionGetCheckTimeout
+     */
     if (aArgs[0] == "checktimeout")
     {
+        /** @cli childsupervision checktimeout (set)
+         * @code
+         * childsupervision checktimeout 30
+         * Done
+         * @endcode
+         * @cparam childsupervision checktimeout @ca{timeout-seconds}
+         * @par api_copy
+         * #otChildSupervisionSetCheckTimeout
+         */
         error = ProcessGetSet(aArgs + 1, otChildSupervisionGetCheckTimeout, otChildSupervisionSetCheckTimeout);
     }
-#if OPENTHREAD_FTD
+    /**
+     * @cli childsupervision interval
+     * @code
+     * childsupervision interval
+     * 30
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otChildSupervisionGetInterval
+     */
     else if (aArgs[0] == "interval")
     {
+        /**
+         * @cli childsupervision interval (set)
+         * @code
+         * childsupervision interval 30
+         * Done
+         * @endcode
+         * @cparam childsupervision interval @ca{interval-seconds}
+         * @par api_copy
+         * #otChildSupervisionSetInterval
+         */
         error = ProcessGetSet(aArgs + 1, otChildSupervisionGetInterval, otChildSupervisionSetInterval);
     }
-#endif
+    else if (aArgs[0] == "failcounter")
+    {
+        if (aArgs[1].IsEmpty())
+        {
+            OutputLine("%u", otChildSupervisionGetCheckFailureCounter(GetInstancePtr()));
+            error = OT_ERROR_NONE;
+        }
+        else if (aArgs[1] == "reset")
+        {
+            otChildSupervisionResetCheckFailureCounter(GetInstancePtr());
+            error = OT_ERROR_NONE;
+        }
+    }
 
     return error;
 }
-#endif // OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
 
+/** @cli childtimeout
+ * @code
+ * childtimeout
+ * 300
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otThreadGetChildTimeout
+ */
 template <> otError Interpreter::Process<Cmd("childtimeout")>(Arg aArgs[])
 {
+    /** @cli childtimeout (set)
+     * @code
+     * childtimeout 300
+     * Done
+     * @endcode
+     * @cparam childtimeout @ca{timeout-seconds}
+     * @par api_copy
+     * #otThreadSetChildTimeout
+     */
     return ProcessGetSet(aArgs, otThreadGetChildTimeout, otThreadSetChildTimeout);
 }
 
 #if OPENTHREAD_CONFIG_COAP_API_ENABLE
-template <> otError Interpreter::Process<Cmd("coap")>(Arg aArgs[])
-{
-    return mCoap.Process(aArgs);
-}
+template <> otError Interpreter::Process<Cmd("coap")>(Arg aArgs[]) { return mCoap.Process(aArgs); }
 #endif
 
 #if OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE
-template <> otError Interpreter::Process<Cmd("coaps")>(Arg aArgs[])
-{
-    return mCoapSecure.Process(aArgs);
-}
+template <> otError Interpreter::Process<Cmd("coaps")>(Arg aArgs[]) { return mCoapSecure.Process(aArgs); }
 #endif
 
 #if OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE
@@ -1376,7 +2220,7 @@
         struct RadioCoexMetricName
         {
             const uint32_t otRadioCoexMetrics::*mValuePtr;
-            const char *                        mName;
+            const char                         *mName;
         };
 
         static const RadioCoexMetricName kTxMetricNames[] = {
@@ -1407,19 +2251,19 @@
         SuccessOrExit(error = otPlatRadioGetCoexMetrics(GetInstancePtr(), &metrics));
 
         OutputLine("Stopped: %s", metrics.mStopped ? "true" : "false");
-        OutputLine("Grant Glitch: %u", metrics.mNumGrantGlitch);
+        OutputLine("Grant Glitch: %lu", ToUlong(metrics.mNumGrantGlitch));
         OutputLine("Transmit metrics");
 
         for (const RadioCoexMetricName &metric : kTxMetricNames)
         {
-            OutputLine(kIndentSize, "%s: %u", metric.mName, metrics.*metric.mValuePtr);
+            OutputLine(kIndentSize, "%s: %lu", metric.mName, ToUlong(metrics.*metric.mValuePtr));
         }
 
         OutputLine("Receive metrics");
 
         for (const RadioCoexMetricName &metric : kRxMetricNames)
         {
-            OutputLine(kIndentSize, "%s: %u", metric.mName, metrics.*metric.mValuePtr);
+            OutputLine(kIndentSize, "%s: %lu", metric.mName, ToUlong(metrics.*metric.mValuePtr));
         }
     }
     else
@@ -1433,22 +2277,177 @@
 #endif // OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE
 
 #if OPENTHREAD_FTD
+/**
+ * @cli contextreusedelay (get,set)
+ * @code
+ * contextreusedelay
+ * 11
+ * Done
+ * @endcode
+ * @code
+ * contextreusedelay 11
+ * Done
+ * @endcode
+ * @cparam contextreusedelay @ca{delay}
+ * Use the optional `delay` argument to set the `CONTEXT_ID_REUSE_DELAY`.
+ * @par
+ * Gets or sets the `CONTEXT_ID_REUSE_DELAY` value.
+ * @sa otThreadGetContextIdReuseDelay
+ * @sa otThreadSetContextIdReuseDelay
+ */
 template <> otError Interpreter::Process<Cmd("contextreusedelay")>(Arg aArgs[])
 {
     return ProcessGetSet(aArgs, otThreadGetContextIdReuseDelay, otThreadSetContextIdReuseDelay);
 }
 #endif
 
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+void Interpreter::OutputBorderRouterCounters(void)
+{
+    struct BrCounterName
+    {
+        const otPacketsAndBytes otBorderRoutingCounters::*mPacketsAndBytes;
+        const char                                       *mName;
+    };
+
+    static const BrCounterName kCounterNames[] = {
+        {&otBorderRoutingCounters::mInboundUnicast, "Inbound Unicast"},
+        {&otBorderRoutingCounters::mInboundMulticast, "Inbound Multicast"},
+        {&otBorderRoutingCounters::mOutboundUnicast, "Outbound Unicast"},
+        {&otBorderRoutingCounters::mOutboundMulticast, "Outbound Multicast"},
+    };
+
+    const otBorderRoutingCounters *brCounters = otIp6GetBorderRoutingCounters(GetInstancePtr());
+    Uint64StringBuffer             uint64StringBuffer;
+
+    for (const BrCounterName &counter : kCounterNames)
+    {
+        OutputFormat("%s:", counter.mName);
+        OutputFormat(" Packets %s",
+                     Uint64ToString((brCounters->*counter.mPacketsAndBytes).mPackets, uint64StringBuffer));
+        OutputLine(" Bytes %s", Uint64ToString((brCounters->*counter.mPacketsAndBytes).mBytes, uint64StringBuffer));
+    }
+
+    OutputLine("RA Rx: %lu", ToUlong(brCounters->mRaRx));
+    OutputLine("RA TxSuccess: %lu", ToUlong(brCounters->mRaTxSuccess));
+    OutputLine("RA TxFailed: %lu", ToUlong(brCounters->mRaTxFailure));
+    OutputLine("RS Rx: %lu", ToUlong(brCounters->mRsRx));
+    OutputLine("RS TxSuccess: %lu", ToUlong(brCounters->mRsTxSuccess));
+    OutputLine("RS TxFailed: %lu", ToUlong(brCounters->mRsTxFailure));
+}
+#endif // OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+
 template <> otError Interpreter::Process<Cmd("counters")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli counters
+     * @code
+     * counters
+     * ip
+     * mac
+     * mle
+     * Done
+     * @endcode
+     * @par
+     * Gets the supported counter names.
+     */
     if (aArgs[0].IsEmpty())
     {
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+        OutputLine("br");
+#endif
         OutputLine("ip");
         OutputLine("mac");
         OutputLine("mle");
     }
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+    /**
+     * @cli counters br
+     * @code
+     * counters br
+     * Inbound Unicast: Packets 4 Bytes 320
+     * Inbound Multicast: Packets 0 Bytes 0
+     * Outbound Unicast: Packets 2 Bytes 160
+     * Outbound Multicast: Packets 0 Bytes 0
+     * RA Rx: 4
+     * RA TxSuccess: 2
+     * RA TxFailed: 0
+     * RS Rx: 0
+     * RS TxSuccess: 2
+     * RS TxFailed: 0
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otIp6GetBorderRoutingCounters
+     */
+    else if (aArgs[0] == "br")
+    {
+        if (aArgs[1].IsEmpty())
+        {
+            OutputBorderRouterCounters();
+        }
+        /**
+         * @cli counters br reset
+         * @code
+         * counters br reset
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otIp6ResetBorderRoutingCounters
+         */
+        else if ((aArgs[1] == "reset") && aArgs[2].IsEmpty())
+        {
+            otIp6ResetBorderRoutingCounters(GetInstancePtr());
+        }
+        else
+        {
+            error = OT_ERROR_INVALID_ARGS;
+        }
+    }
+#endif
+    /**
+     * @cli counters (mac)
+     * @code
+     * counters mac
+     * TxTotal: 10
+     *    TxUnicast: 3
+     *    TxBroadcast: 7
+     *    TxAckRequested: 3
+     *    TxAcked: 3
+     *    TxNoAckRequested: 7
+     *    TxData: 10
+     *    TxDataPoll: 0
+     *    TxBeacon: 0
+     *    TxBeaconRequest: 0
+     *    TxOther: 0
+     *    TxRetry: 0
+     *    TxErrCca: 0
+     *    TxErrBusyChannel: 0
+     * RxTotal: 2
+     *    RxUnicast: 1
+     *    RxBroadcast: 1
+     *    RxData: 2
+     *    RxDataPoll: 0
+     *    RxBeacon: 0
+     *    RxBeaconRequest: 0
+     *    RxOther: 0
+     *    RxAddressFiltered: 0
+     *    RxDestAddrFiltered: 0
+     *    RxDuplicated: 0
+     *    RxErrNoFrame: 0
+     *    RxErrNoUnknownNeighbor: 0
+     *    RxErrInvalidSrcAddr: 0
+     *    RxErrSec: 0
+     *    RxErrFcs: 0
+     *    RxErrOther: 0
+     * Done
+     * @endcode
+     * @cparam counters @ca{mac}
+     * @par api_copy
+     * #otLinkGetCounters
+     */
     else if (aArgs[0] == "mac")
     {
         if (aArgs[1].IsEmpty())
@@ -1456,7 +2455,7 @@
             struct MacCounterName
             {
                 const uint32_t otMacCounters::*mValuePtr;
-                const char *                   mName;
+                const char                    *mName;
             };
 
             static const MacCounterName kTxCounterNames[] = {
@@ -1473,6 +2472,9 @@
                 {&otMacCounters::mTxRetry, "TxRetry"},
                 {&otMacCounters::mTxErrCca, "TxErrCca"},
                 {&otMacCounters::mTxErrBusyChannel, "TxErrBusyChannel"},
+                {&otMacCounters::mTxErrAbort, "TxErrAbort"},
+                {&otMacCounters::mTxDirectMaxRetryExpiry, "TxDirectMaxRetryExpiry"},
+                {&otMacCounters::mTxIndirectMaxRetryExpiry, "TxIndirectMaxRetryExpiry"},
             };
 
             static const MacCounterName kRxCounterNames[] = {
@@ -1496,20 +2498,30 @@
 
             const otMacCounters *macCounters = otLinkGetCounters(GetInstancePtr());
 
-            OutputLine("TxTotal: %d", macCounters->mTxTotal);
+            OutputLine("TxTotal: %lu", ToUlong(macCounters->mTxTotal));
 
             for (const MacCounterName &counter : kTxCounterNames)
             {
-                OutputLine(kIndentSize, "%s: %u", counter.mName, macCounters->*counter.mValuePtr);
+                OutputLine(kIndentSize, "%s: %lu", counter.mName, ToUlong(macCounters->*counter.mValuePtr));
             }
 
-            OutputLine("RxTotal: %d", macCounters->mRxTotal);
+            OutputLine("RxTotal: %lu", ToUlong(macCounters->mRxTotal));
 
             for (const MacCounterName &counter : kRxCounterNames)
             {
-                OutputLine(kIndentSize, "%s: %u", counter.mName, macCounters->*counter.mValuePtr);
+                OutputLine(kIndentSize, "%s: %lu", counter.mName, ToUlong(macCounters->*counter.mValuePtr));
             }
         }
+        /**
+         * @cli counters mac reset
+         * @code
+         * counters mac reset
+         * Done
+         * @endcode
+         * @cparam counters @ca{mac} reset
+         * @par api_copy
+         * #otLinkResetCounters
+         */
         else if ((aArgs[1] == "reset") && aArgs[2].IsEmpty())
         {
             otLinkResetCounters(GetInstancePtr());
@@ -1519,6 +2531,25 @@
             error = OT_ERROR_INVALID_ARGS;
         }
     }
+    /**
+     * @cli counters (mle)
+     * @code
+     * counters mle
+     * Role Disabled: 0
+     * Role Detached: 1
+     * Role Child: 0
+     * Role Router: 0
+     * Role Leader: 1
+     * Attach Attempts: 1
+     * Partition Id Changes: 1
+     * Better Partition Attach Attempts: 0
+     * Parent Changes: 0
+     * Done
+     * @endcode
+     * @cparam counters @ca{mle}
+     * @par api_copy
+     * #otThreadGetMleCounters
+     */
     else if (aArgs[0] == "mle")
     {
         if (aArgs[1].IsEmpty())
@@ -1526,7 +2557,7 @@
             struct MleCounterName
             {
                 const uint16_t otMleCounters::*mValuePtr;
-                const char *                   mName;
+                const char                    *mName;
             };
 
             static const MleCounterName kCounterNames[] = {
@@ -1545,9 +2576,43 @@
 
             for (const MleCounterName &counter : kCounterNames)
             {
-                OutputLine("%s: %d", counter.mName, mleCounters->*counter.mValuePtr);
+                OutputLine("%s: %u", counter.mName, mleCounters->*counter.mValuePtr);
             }
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+            {
+                struct MleTimeCounterName
+                {
+                    const uint64_t otMleCounters::*mValuePtr;
+                    const char                    *mName;
+                };
+
+                static const MleTimeCounterName kTimeCounterNames[] = {
+                    {&otMleCounters::mDisabledTime, "Disabled"}, {&otMleCounters::mDetachedTime, "Detached"},
+                    {&otMleCounters::mChildTime, "Child"},       {&otMleCounters::mRouterTime, "Router"},
+                    {&otMleCounters::mLeaderTime, "Leader"},
+                };
+
+                for (const MleTimeCounterName &counter : kTimeCounterNames)
+                {
+                    OutputFormat("Time %s Milli: ", counter.mName);
+                    OutputUint64Line(mleCounters->*counter.mValuePtr);
+                }
+
+                OutputFormat("Time Tracked Milli: ");
+                OutputUint64Line(mleCounters->mTrackedTime);
+            }
+#endif
         }
+        /**
+         * @cli counters mle reset
+         * @code
+         * counters mle reset
+         * Done
+         * @endcode
+         * @cparam counters @ca{mle} reset
+         * @par api_copy
+         * #otThreadResetMleCounters
+         */
         else if ((aArgs[1] == "reset") && aArgs[2].IsEmpty())
         {
             otThreadResetMleCounters(GetInstancePtr());
@@ -1557,6 +2622,20 @@
             error = OT_ERROR_INVALID_ARGS;
         }
     }
+    /**
+     * @cli counters ip
+     * @code
+     * counters ip
+     * TxSuccess: 10
+     * TxFailed: 0
+     * RxSuccess: 5
+     * RxFailed: 0
+     * Done
+     * @endcode
+     * @cparam counters @ca{ip}
+     * @par api_copy
+     * #otThreadGetIp6Counters
+     */
     else if (aArgs[0] == "ip")
     {
         if (aArgs[1].IsEmpty())
@@ -1564,7 +2643,7 @@
             struct IpCounterName
             {
                 const uint32_t otIpCounters::*mValuePtr;
-                const char *                  mName;
+                const char                   *mName;
             };
 
             static const IpCounterName kCounterNames[] = {
@@ -1578,9 +2657,19 @@
 
             for (const IpCounterName &counter : kCounterNames)
             {
-                OutputLine("%s: %d", counter.mName, ipCounters->*counter.mValuePtr);
+                OutputLine("%s: %lu", counter.mName, ToUlong(ipCounters->*counter.mValuePtr));
             }
         }
+        /**
+         * @cli counters ip reset
+         * @code
+         * counters ip reset
+         * Done
+         * @endcode
+         * @cparam counters @ca{ip} reset
+         * @par api_copy
+         * #otThreadResetIp6Counters
+         */
         else if ((aArgs[1] == "reset") && aArgs[2].IsEmpty())
         {
             otThreadResetIp6Counters(GetInstancePtr());
@@ -1603,21 +2692,67 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli csl
+     * @code
+     * csl
+     * Channel: 11
+     * Period: 1000 (in units of 10 symbols), 160ms
+     * Timeout: 1000s
+     * Done
+     * @endcode
+     * @par
+     * Gets the CSL configuration.
+     * @sa otLinkCslGetChannel
+     * @sa otLinkCslGetPeriod
+     * @sa otLinkCslGetPeriod
+     * @sa otLinkCslGetTimeout
+     */
     if (aArgs[0].IsEmpty())
     {
         OutputLine("Channel: %u", otLinkCslGetChannel(GetInstancePtr()));
-        OutputLine("Period: %u(in units of 10 symbols), %ums", otLinkCslGetPeriod(GetInstancePtr()),
-                   otLinkCslGetPeriod(GetInstancePtr()) * kUsPerTenSymbols / 1000);
-        OutputLine("Timeout: %us", otLinkCslGetTimeout(GetInstancePtr()));
+        OutputLine("Period: %u(in units of 10 symbols), %lums", otLinkCslGetPeriod(GetInstancePtr()),
+                   ToUlong(otLinkCslGetPeriod(GetInstancePtr()) * kUsPerTenSymbols / 1000));
+        OutputLine("Timeout: %lus", ToUlong(otLinkCslGetTimeout(GetInstancePtr())));
     }
+    /**
+     * @cli csl channel
+     * @code
+     * csl channel 20
+     * Done
+     * @endcode
+     * @cparam csl channel @ca{channel}
+     * @par api_copy
+     * #otLinkCslSetChannel
+     */
     else if (aArgs[0] == "channel")
     {
         error = ProcessSet(aArgs + 1, otLinkCslSetChannel);
     }
+    /**
+     * @cli csl period
+     * @code
+     * csl period 3000
+     * Done
+     * @endcode
+     * @cparam csl period @ca{period}
+     * @par api_copy
+     * #otLinkCslSetPeriod
+     */
     else if (aArgs[0] == "period")
     {
         error = ProcessSet(aArgs + 1, otLinkCslSetPeriod);
     }
+    /**
+     * @cli csl timeout
+     * @code
+     * cls timeout 10
+     * Done
+     * @endcode
+     * @cparam csl timeout @ca{timeout}
+     * @par api_copy
+     * #otLinkCslSetTimeout
+     */
     else if (aArgs[0] == "timeout")
     {
         error = ProcessSet(aArgs + 1, otLinkCslSetTimeout);
@@ -1636,10 +2771,32 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli delaytimermin
+     * @code
+     * delaytimermin
+     * 30
+     * Done
+     * @endcode
+     * @par
+     * Get the minimal delay timer (in seconds).
+     * @sa otDatasetGetDelayTimerMinimal
+     */
     if (aArgs[0].IsEmpty())
     {
-        OutputLine("%d", (otDatasetGetDelayTimerMinimal(GetInstancePtr()) / 1000));
+        OutputLine("%lu", ToUlong((otDatasetGetDelayTimerMinimal(GetInstancePtr()) / 1000)));
     }
+    /**
+     * @cli delaytimermin (set)
+     * @code
+     * delaytimermin 60
+     * Done
+     * @endcode
+     * @cparam delaytimermin @ca{delaytimermin}
+     * @par
+     * Sets the minimal delay timer (in seconds).
+     * @sa otDatasetSetDelayTimerMinimal
+     */
     else if (aArgs[1].IsEmpty())
     {
         uint32_t delay;
@@ -1675,11 +2832,57 @@
     return error;
 }
 
+/**
+ * @cli discover
+ * @code
+ * discover
+ * | J | Network Name     | Extended PAN     | PAN  | MAC Address      | Ch | dBm | LQI |
+ * +---+------------------+------------------+------+------------------+----+-----+-----+
+ * | 0 | OpenThread       | dead00beef00cafe | ffff | f1d92a82c8d8fe43 | 11 | -20 |   0 |
+ * Done
+ * @endcode
+ * @cparam discover [@ca{channel}]
+ * `channel`: The channel to discover on. If no channel is provided, the discovery will cover all
+ * valid channels.
+ * @par
+ * Perform an MLE Discovery operation.
+ * @sa otThreadDiscover
+ */
 template <> otError Interpreter::Process<Cmd("discover")>(Arg aArgs[])
 {
     otError  error        = OT_ERROR_NONE;
     uint32_t scanChannels = 0;
 
+#if OPENTHREAD_FTD
+    /**
+     * @cli discover reqcallback (enable,disable)
+     * @code
+     * discover reqcallback enable
+     * Done
+     * @endcode
+     * @cparam discover reqcallback @ca{enable|disable}
+     * @par api_copy
+     * #otThreadSetDiscoveryRequestCallback
+     */
+    if (aArgs[0] == "reqcallback")
+    {
+        bool                             enable;
+        otThreadDiscoveryRequestCallback callback = nullptr;
+        void                            *context  = nullptr;
+
+        SuccessOrExit(error = ParseEnableOrDisable(aArgs[1], enable));
+
+        if (enable)
+        {
+            callback = &Interpreter::HandleDiscoveryRequest;
+            context  = this;
+        }
+
+        otThreadSetDiscoveryRequestCallback(GetInstancePtr(), callback, context);
+        ExitNow();
+    }
+#endif // OPENTHREAD_FTD
+
     if (!aArgs[0].IsEmpty())
     {
         uint8_t channel;
@@ -1722,9 +2925,49 @@
     {
         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());
@@ -1739,25 +2982,124 @@
     }
 #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
+         * 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[1].IsEmpty())
         {
             const otDnsQueryConfig *defaultConfig = otDnsClientGetDefaultConfig(GetInstancePtr());
 
             OutputFormat("Server: ");
             OutputSockAddrLine(defaultConfig->mServerSockAddr);
-            OutputLine("ResponseTimeout: %u ms", defaultConfig->mResponseTimeout);
+            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
         {
             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);
@@ -1777,6 +3119,51 @@
     }
 #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);
@@ -1785,6 +3172,29 @@
                                                 &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);
@@ -1795,6 +3205,55 @@
     }
 #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);
@@ -1806,20 +3265,85 @@
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
 
+const char *Interpreter::DnsConfigServiceModeToString(otDnsServiceMode aMode) const
+{
+    static const char *const 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");
+
+    return Stringify(aMode, kServiceModeStrings);
+}
+
+otError Interpreter::ParseDnsServiceMode(const Arg &aArg, otDnsServiceMode &aMode) const
+{
+    otError error = OT_ERROR_NONE;
+
+    if (aArg == "def")
+    {
+        aMode = OT_DNS_SERVICE_MODE_UNSPECIFIED;
+    }
+    else if (aArg == "srv")
+    {
+        aMode = OT_DNS_SERVICE_MODE_SRV;
+    }
+    else if (aArg == "txt")
+    {
+        aMode = OT_DNS_SERVICE_MODE_TXT;
+    }
+    else if (aArg == "srv_txt")
+    {
+        aMode = OT_DNS_SERVICE_MODE_SRV_TXT;
+    }
+    else if (aArg == "srv_txt_sep")
+    {
+        aMode = OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE;
+    }
+    else if (aArg == "srv_txt_opt")
+    {
+        aMode = OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE;
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_ARGS;
+    }
+
+    return error;
+}
+
 otError Interpreter::GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig)
 {
     // This method gets the optional DNS config from `aArgs[]`.
-    // The format: `[server IPv6 address] [server port] [timeout]
-    // [max tx attempt] [recursion desired]`.
+    // 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 = aArgs[0].ParseAsIp6Address(aConfig->mServerSockAddr.mAddress));
+    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));
@@ -1834,6 +3358,24 @@
     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;
 }
@@ -1860,12 +3402,12 @@
         while (otDnsAddressResponseGetAddress(aResponse, index, &address, &ttl) == OT_ERROR_NONE)
         {
             OutputIp6Address(address);
-            OutputFormat(" TTL:%u ", ttl);
+            OutputFormat(" TTL:%lu ", ToUlong(ttl));
             index++;
         }
     }
 
-    OutputLine("");
+    OutputNewLine();
     OutputResult(aError);
 }
 
@@ -1873,15 +3415,26 @@
 
 void Interpreter::OutputDnsServiceInfo(uint8_t aIndentSize, const otDnsServiceInfo &aServiceInfo)
 {
-    OutputLine(aIndentSize, "Port:%d, Priority:%d, Weight:%d, TTL:%u", aServiceInfo.mPort, aServiceInfo.mPriority,
-               aServiceInfo.mWeight, aServiceInfo.mTtl);
+    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:%u", aServiceInfo.mHostAddressTtl);
+    OutputLine(" TTL:%lu", ToUlong(aServiceInfo.mHostAddressTtl));
     OutputFormat(aIndentSize, "TXT:");
-    OutputDnsTxtData(aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize);
-    OutputLine(" TTL:%u", aServiceInfo.mTxtDataTtl);
+
+    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)
@@ -1893,7 +3446,7 @@
 {
     char             name[OT_DNS_MAX_NAME_SIZE];
     char             label[OT_DNS_MAX_LABEL_SIZE];
-    uint8_t          txtBuffer[255];
+    uint8_t          txtBuffer[kMaxTxtDataSize];
     otDnsServiceInfo serviceInfo;
 
     IgnoreError(otDnsBrowseResponseGetServiceName(aResponse, name, sizeof(name)));
@@ -1919,7 +3472,7 @@
                 OutputDnsServiceInfo(kIndentSize, serviceInfo);
             }
 
-            OutputLine("");
+            OutputNewLine();
         }
     }
 
@@ -1935,7 +3488,7 @@
 {
     char             name[OT_DNS_MAX_NAME_SIZE];
     char             label[OT_DNS_MAX_LABEL_SIZE];
-    uint8_t          txtBuffer[255];
+    uint8_t          txtBuffer[kMaxTxtDataSize];
     otDnsServiceInfo serviceInfo;
 
     IgnoreError(otDnsServiceResponseGetServiceName(aResponse, label, sizeof(label), name, sizeof(name)));
@@ -1951,8 +3504,8 @@
 
         if (otDnsServiceResponseGetServiceInfo(aResponse, &serviceInfo) == OT_ERROR_NONE)
         {
-            OutputDnsServiceInfo(/* aIndetSize */ 0, serviceInfo);
-            OutputLine("");
+            OutputDnsServiceInfo(/* aIndentSize */ 0, serviceInfo);
+            OutputNewLine();
         }
     }
 
@@ -1986,7 +3539,7 @@
     {
         if (aEntry.mValidLastTrans)
         {
-            OutputFormat(" transTime=%u eid=", aEntry.mLastTransTime);
+            OutputFormat(" transTime=%lu eid=", ToUlong(aEntry.mLastTransTime));
             OutputIp6Address(aEntry.mMeshLocalEid);
         }
     }
@@ -2000,9 +3553,21 @@
         OutputFormat(" retryDelay=%u", aEntry.mRetryDelay);
     }
 
-    OutputLine("");
+    OutputNewLine();
 }
 
+/**
+ * @cli eidcache
+ * @code
+ * eidcache
+ * fd49:caf4:a29f:dc0e:97fc:69dd:3c16:df7d 2000 cache canEvict=1 transTime=0 eid=fd49:caf4:a29f:dc0e:97fc:69dd:3c16:df7d
+ * fd49:caf4:a29f:dc0e:97fc:69dd:3c16:df7f fffe retry canEvict=1 timeout=10 retryDelay=30
+ * Done
+ * @endcode
+ * @par
+ * Returns the EID-to-RLOC cache entries.
+ * @sa otThreadGetNextCacheEntry
+ */
 template <> otError Interpreter::Process<Cmd("eidcache")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
@@ -2012,7 +3577,7 @@
 
     memset(&iterator, 0, sizeof(iterator));
 
-    for (uint8_t i = 0;; i++)
+    while (true)
     {
         SuccessOrExit(otThreadGetNextCacheEntry(GetInstancePtr(), &entry, &iterator));
         OutputEidCacheEntry(entry);
@@ -2053,10 +3618,31 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli extaddr
+     * @code
+     * extaddr
+     * dead00beef00cafe
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otLinkGetExtendedAddress
+     */
     if (aArgs[0].IsEmpty())
     {
         OutputExtAddressLine(*otLinkGetExtendedAddress(GetInstancePtr()));
     }
+    /**
+     * @cli extaddr (set)
+     * @code
+     * extaddr dead00beef00cafe
+     * dead00beef00cafe
+     * Done
+     * @endcode
+     * @cparam extaddr @ca{extaddr}
+     * @par api_copy
+     * #otLinkSetExtendedAddress
+     */
     else
     {
         otExtAddress extAddress;
@@ -2095,7 +3681,7 @@
 #if (OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_DEBUG_UART) && OPENTHREAD_POSIX
     else if (aArgs[0] == "filename")
     {
-        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+        VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
         SuccessOrExit(error = otPlatDebugUart_logfile(aArgs[1].GetCString()));
     }
 #endif
@@ -2112,10 +3698,33 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli extpanid
+     * @code
+     * extpanid
+     * dead00beef00cafe
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetExtendedPanId
+     */
     if (aArgs[0].IsEmpty())
     {
         OutputBytesLine(otThreadGetExtendedPanId(GetInstancePtr())->m8);
     }
+    /**
+     * @cli extpanid (set)
+     * @code
+     * extpanid dead00beef00cafe
+     * Done
+     * @endcode
+     * @cparam extpanid @ca{extpanid}
+     * @par
+     * @note The current commissioning credential becomes stale after changing this value.
+     * Use `pskc` to reset.
+     * @par api_copy
+     * #otThreadSetExtendedPanId
+     */
     else
     {
         otExtendedPanId extPanId;
@@ -2128,6 +3737,14 @@
     return error;
 }
 
+/**
+ * @cli factoryreset
+ * @code
+ * factoryreset
+ * @endcode
+ * @par api_copy
+ * #otInstanceFactoryReset
+ */
 template <> otError Interpreter::Process<Cmd("factoryreset")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
@@ -2142,6 +3759,19 @@
 {
     otError error = OT_ERROR_INVALID_COMMAND;
 
+    /**
+     * @cli fake (a,an)
+     * @code
+     * fake /a/an fdde:ad00:beef:0:0:ff:fe00:a800 fd00:7d03:7d03:7d03:55f2:bb6a:7a43:a03b 1111222233334444
+     * Done
+     * @endcode
+     * @cparam fake /a/an @ca{dst-ipaddr} @ca{target} @ca{meshLocalIid}
+     * @par
+     * Sends fake Thread messages.
+     * @par
+     * Available when `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is enabled.
+     * @sa otThreadSendAddressNotification
+     */
     if (aArgs[0] == "/a/an")
     {
         otIp6Address             destination, target;
@@ -2176,6 +3806,17 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli fem
+     * @code
+     * fem
+     * LNA gain 11 dBm
+     * Done
+     * @endcode
+     * @par
+     * Gets external FEM parameters.
+     * @sa otPlatRadioGetFemLnaGain
+     */
     if (aArgs[0].IsEmpty())
     {
         int8_t lnaGain;
@@ -2183,6 +3824,16 @@
         SuccessOrExit(error = otPlatRadioGetFemLnaGain(GetInstancePtr(), &lnaGain));
         OutputLine("LNA gain %d dBm", lnaGain);
     }
+    /**
+     * @cli fem lnagain (get)
+     * @code
+     * fem lnagain
+     * 11
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otPlatRadioGetFemLnaGain
+     */
     else if (aArgs[0] == "lnagain")
     {
         if (aArgs[1].IsEmpty())
@@ -2192,6 +3843,15 @@
             SuccessOrExit(error = otPlatRadioGetFemLnaGain(GetInstancePtr(), &lnaGain));
             OutputLine("%d", lnaGain);
         }
+        /**
+         * @cli fem lnagain (set)
+         * @code
+         * fem lnagain 8
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otPlatRadioSetFemLnaGain
+         */
         else
         {
             int8_t lnaGain;
@@ -2213,6 +3873,21 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli ifconfig
+     * @code
+     * ifconfig
+     * down
+     * Done
+     * @endcode
+     * @code
+     * ifconfig
+     * up
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otIp6IsEnabled
+     */
     if (aArgs[0].IsEmpty())
     {
         if (otIp6IsEnabled(GetInstancePtr()))
@@ -2224,6 +3899,20 @@
             OutputLine("down");
         }
     }
+    /**
+     * @cli ifconfig (up,down)
+     * @code
+     * ifconfig up
+     * Done
+     * @endcode
+     * @code
+     * ifconfig down
+     * Done
+     * @endcode
+     * @cparam ifconfig @ca{up|down}
+     * @par api_copy
+     * #otIp6SetEnabled
+     */
     else if (aArgs[0] == "up")
     {
         SuccessOrExit(error = otIp6SetEnabled(GetInstancePtr(), true));
@@ -2269,6 +3958,27 @@
         verbose = true;
     }
 
+    /**
+     * @cli ipaddr
+     * @code
+     * ipaddr
+     * fdde:ad00:beef:0:0:ff:fe00:0
+     * fdde:ad00:beef:0:558:f56b:d688:799
+     * fe80:0:0:0:f3d9:2a82:c8d8:fe43
+     * Done
+     * @endcode
+     * @code
+     * ipaddr -v
+     * fdde:ad00:beef:0:0:ff:fe00:0 origin:thread
+     * fdde:ad00:beef:0:558:f56b:d688:799 origin:thread
+     * fe80:0:0:0:f3d9:2a82:c8d8:fe43 origin:thread
+     * Done
+     * @endcode
+     * @cparam ipaddr [@ca{-v}]
+     * Use `-v` to get verbose IP Address information.
+     * @par api_copy
+     * #otIp6GetUnicastAddresses
+     */
     if (aArgs[0].IsEmpty())
     {
         const otNetifAddress *unicastAddrs = otIp6GetUnicastAddresses(GetInstancePtr());
@@ -2282,9 +3992,19 @@
                 OutputFormat(" origin:%s", AddressOriginToString(addr->mAddressOrigin));
             }
 
-            OutputLine("");
+            OutputNewLine();
         }
     }
+    /**
+     * @cli ipaddr add
+     * @code
+     * ipaddr add 2001::dead:beef:cafe
+     * Done
+     * @endcode
+     * @cparam ipaddr add @ca{aAddress}
+     * @par api_copy
+     * #otIp6AddUnicastAddress
+     */
     else if (aArgs[0] == "add")
     {
         otNetifAddress address;
@@ -2297,6 +4017,16 @@
 
         error = otIp6AddUnicastAddress(GetInstancePtr(), &address);
     }
+    /**
+     * @cli ipaddr del
+     * @code
+     * ipaddr del 2001::dead:beef:cafe
+     * Done
+     * @endcode
+     * @cparam ipaddr del @ca{aAddress}
+     * @par api_copy
+     * #otIp6RemoveUnicastAddress
+     */
     else if (aArgs[0] == "del")
     {
         otIp6Address address;
@@ -2304,14 +4034,44 @@
         SuccessOrExit(error = aArgs[1].ParseAsIp6Address(address));
         error = otIp6RemoveUnicastAddress(GetInstancePtr(), &address);
     }
+    /**
+     * @cli ipaddr linklocal
+     * @code
+     * ipaddr linklocal
+     * fe80:0:0:0:f3d9:2a82:c8d8:fe43
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetLinkLocalIp6Address
+     */
     else if (aArgs[0] == "linklocal")
     {
         OutputIp6AddressLine(*otThreadGetLinkLocalIp6Address(GetInstancePtr()));
     }
+    /**
+     * @cli ipaddr rloc
+     * @code
+     * ipaddr rloc
+     * fdde:ad00:beef:0:0:ff:fe00:0
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetRloc
+     */
     else if (aArgs[0] == "rloc")
     {
         OutputIp6AddressLine(*otThreadGetRloc(GetInstancePtr()));
     }
+    /**
+     * @cli ipaddr mleid
+     * @code
+     * ipaddr mleid
+     * fdde:ad00:beef:0:558:f56b:d688:799
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetMeshLocalEid
+     */
     else if (aArgs[0] == "mleid")
     {
         OutputIp6AddressLine(*otThreadGetMeshLocalEid(GetInstancePtr()));
@@ -2329,6 +4089,18 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli ipmaddr
+     * @code
+     * ipmaddr
+     * ff05:0:0:0:0:0:0:1
+     * ff33:40:fdde:ad00:beef:0:0:1
+     * ff32:40:fdde:ad00:beef:0:0:1
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otIp6GetMulticastAddresses
+     */
     if (aArgs[0].IsEmpty())
     {
         for (const otNetifMulticastAddress *addr = otIp6GetMulticastAddresses(GetInstancePtr()); addr;
@@ -2337,6 +4109,16 @@
             OutputIp6AddressLine(addr->mAddress);
         }
     }
+    /**
+     * @cli ipmaddr add
+     * @code
+     * ipmaddr add ff05::1
+     * Done
+     * @endcode
+     * @cparam ipmaddr add @ca{aAddress}
+     * @par api_copy
+     * #otIp6SubscribeMulticastAddress
+     */
     else if (aArgs[0] == "add")
     {
         otIp6Address address;
@@ -2354,6 +4136,16 @@
         while (false);
 #endif
     }
+    /**
+     * @cli ipmaddr del
+     * @code
+     * ipmaddr del ff05::1
+     * Done
+     * @endcode
+     * @cparam ipmaddr del @ca{aAddress}
+     * @par api_copy
+     * #otIp6UnsubscribeMulticastAddress
+     */
     else if (aArgs[0] == "del")
     {
         otIp6Address address;
@@ -2361,12 +4153,36 @@
         SuccessOrExit(error = aArgs[1].ParseAsIp6Address(address));
         error = otIp6UnsubscribeMulticastAddress(GetInstancePtr(), &address);
     }
+    /**
+     * @cli ipmaddr promiscuous
+     * @code
+     * ipmaddr promiscuous
+     * Disabled
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otIp6IsMulticastPromiscuousEnabled
+     */
     else if (aArgs[0] == "promiscuous")
     {
         if (aArgs[1].IsEmpty())
         {
             OutputEnabledDisabledStatus(otIp6IsMulticastPromiscuousEnabled(GetInstancePtr()));
         }
+        /**
+         * @cli ipmaddr promiscuous (enable,disable)
+         * @code
+         * ipmaddr promiscuous enable
+         * Done
+         * @endcode
+         * @code
+         * ipmaddr promiscuous disable
+         * Done
+         * @endcode
+         * @cparam ipmaddr promiscuous @ca{enable|disable}
+         * @par api_copy
+         * #otIp6SetMulticastPromiscuousEnabled
+         */
         else
         {
             bool enable;
@@ -2375,10 +4191,30 @@
             otIp6SetMulticastPromiscuousEnabled(GetInstancePtr(), enable);
         }
     }
+    /**
+     * @cli ipmaddr llatn
+     * @code
+     * ipmaddr llatn
+     * ff32:40:fdde:ad00:beef:0:0:1
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetLinkLocalAllThreadNodesMulticastAddress
+     */
     else if (aArgs[0] == "llatn")
     {
         OutputIp6AddressLine(*otThreadGetLinkLocalAllThreadNodesMulticastAddress(GetInstancePtr()));
     }
+    /**
+     * @cli ipmaddr rlatn
+     * @code
+     * ipmaddr rlatn
+     * ff33:40:fdde:ad00:beef:0:0:1
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetRealmLocalAllThreadNodesMulticastAddress
+     */
     else if (aArgs[0] == "rlatn")
     {
         OutputIp6AddressLine(*otThreadGetRealmLocalAllThreadNodesMulticastAddress(GetInstancePtr()));
@@ -2396,18 +4232,74 @@
 {
     otError error = OT_ERROR_INVALID_ARGS;
 
+    /**
+     * @cli keysequence counter
+     * @code
+     * keysequence counter
+     * 10
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetKeySequenceCounter
+     */
     if (aArgs[0] == "counter")
     {
+        /**
+         * @cli keysequence counter (set)
+         * @code
+         * keysequence counter 10
+         * Done
+         * @endcode
+         * @cparam keysequence counter @ca{counter}
+         * @par api_copy
+         * #otThreadSetKeySequenceCounter
+         */
         error = ProcessGetSet(aArgs + 1, otThreadGetKeySequenceCounter, otThreadSetKeySequenceCounter);
     }
+    /**
+     * @cli keysequence guardtime
+     * @code
+     * keysequence guardtime
+     * 0
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetKeySwitchGuardTime
+     */
     else if (aArgs[0] == "guardtime")
     {
+        /**
+         * @cli keysequence guardtime (set)
+         * @code
+         * keysequence guardtime 0
+         * Done
+         * @endcode
+         * @cparam keysequence guardtime @ca{guardtime-hours}
+         * Use `0` to `Thread Key Switch` immediately if there's a key index match.
+         * @par api_copy
+         * #otThreadSetKeySwitchGuardTime
+         */
         error = ProcessGetSet(aArgs + 1, otThreadGetKeySwitchGuardTime, otThreadSetKeySwitchGuardTime);
     }
 
     return error;
 }
 
+/**
+ * @cli leaderdata
+ * @code
+ * leaderdata
+ * Partition ID: 1077744240
+ * Weighting: 64
+ * Data Version: 109
+ * Stable Data Version: 211
+ * Leader Router ID: 60
+ * Done
+ * @endcode
+ * @par
+ * Gets the Thread Leader Data.
+ * @sa otThreadGetLeaderData
+ */
 template <> otError Interpreter::Process<Cmd("leaderdata")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
@@ -2417,11 +4309,11 @@
 
     SuccessOrExit(error = otThreadGetLeaderData(GetInstancePtr(), &leaderData));
 
-    OutputLine("Partition ID: %u", leaderData.mPartitionId);
-    OutputLine("Weighting: %d", leaderData.mWeighting);
-    OutputLine("Data Version: %d", leaderData.mDataVersion);
-    OutputLine("Stable Data Version: %d", leaderData.mStableDataVersion);
-    OutputLine("Leader Router ID: %d", leaderData.mLeaderRouterId);
+    OutputLine("Partition ID: %lu", ToUlong(leaderData.mPartitionId));
+    OutputLine("Weighting: %u", leaderData.mWeighting);
+    OutputLine("Data Version: %u", leaderData.mDataVersion);
+    OutputLine("Stable Data Version: %u", leaderData.mStableDataVersion);
+    OutputLine("Leader Router ID: %u", leaderData.mLeaderRouterId);
 
 exit:
     return error;
@@ -2432,12 +4324,40 @@
 {
     otError error = OT_ERROR_INVALID_COMMAND;
 
+    /**
+     * @cli partitionid
+     * @code
+     * partitionid
+     * 4294967295
+     * Done
+     * @endcode
+     * @par
+     * Get the Thread Network Partition ID.
+     * @sa otThreadGetPartitionId
+     */
     if (aArgs[0].IsEmpty())
     {
-        OutputLine("%u", otThreadGetPartitionId(GetInstancePtr()));
+        OutputLine("%lu", ToUlong(otThreadGetPartitionId(GetInstancePtr())));
         error = OT_ERROR_NONE;
     }
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+    /**
+     * @cli partitionid preferred (get,set)
+     * @code
+     * partitionid preferred
+     * 4294967295
+     * Done
+     * @endcode
+     * @code
+     * partitionid preferred 0xffffffff
+     * Done
+     * @endcode
+     * @cparam partitionid preferred @ca{partitionid}
+     * @sa otThreadGetPreferredLeaderPartitionId
+     * @sa otThreadSetPreferredLeaderPartitionId
+     * @par
+     * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is required.
+     */
     else if (aArgs[0] == "preferred")
     {
         error = ProcessGetSet(aArgs + 1, otThreadGetPreferredLeaderPartitionId, otThreadSetPreferredLeaderPartitionId);
@@ -2447,39 +4367,158 @@
     return error;
 }
 
+/**
+ * @cli leaderweight
+ * @code
+ * leaderweight
+ * 128
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otThreadGetLocalLeaderWeight
+ */
 template <> otError Interpreter::Process<Cmd("leaderweight")>(Arg aArgs[])
 {
+    /**
+     * @cli leaderweight (set)
+     * @code
+     * leaderweight 128
+     * Done
+     * @endcode
+     * @cparam leaderweight @ca{weight}
+     * @par api_copy
+     * #otThreadSetLocalLeaderWeight
+     */
     return ProcessGetSet(aArgs, otThreadGetLocalLeaderWeight, otThreadSetLocalLeaderWeight);
 }
+
+template <> otError Interpreter::Process<Cmd("deviceprops")>(Arg aArgs[])
+{
+    static const char *const kPowerSupplyStrings[4] = {
+        "battery",           // (0) OT_POWER_SUPPLY_BATTERY
+        "external",          // (1) OT_POWER_SUPPLY_EXTERNAL
+        "external-stable",   // (2) OT_POWER_SUPPLY_EXTERNAL_STABLE
+        "external-unstable", // (3) OT_POWER_SUPPLY_EXTERNAL_UNSTABLE
+    };
+
+    static_assert(0 == OT_POWER_SUPPLY_BATTERY, "OT_POWER_SUPPLY_BATTERY value is incorrect");
+    static_assert(1 == OT_POWER_SUPPLY_EXTERNAL, "OT_POWER_SUPPLY_EXTERNAL value is incorrect");
+    static_assert(2 == OT_POWER_SUPPLY_EXTERNAL_STABLE, "OT_POWER_SUPPLY_EXTERNAL_STABLE value is incorrect");
+    static_assert(3 == OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, "OT_POWER_SUPPLY_EXTERNAL_UNSTABLE value is incorrect");
+
+    otError error = OT_ERROR_NONE;
+
+    /**
+     * @cli deviceprops
+     * @code
+     * deviceprops
+     * PowerSupply      : external
+     * IsBorderRouter   : yes
+     * SupportsCcm      : no
+     * IsUnstable       : no
+     * WeightAdjustment : 0
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetDeviceProperties
+     */
+    if (aArgs[0].IsEmpty())
+    {
+        const otDeviceProperties *props = otThreadGetDeviceProperties(GetInstancePtr());
+
+        OutputLine("PowerSupply      : %s", Stringify(props->mPowerSupply, kPowerSupplyStrings));
+        OutputLine("IsBorderRouter   : %s", props->mIsBorderRouter ? "yes" : "no");
+        OutputLine("SupportsCcm      : %s", props->mSupportsCcm ? "yes" : "no");
+        OutputLine("IsUnstable       : %s", props->mIsUnstable ? "yes" : "no");
+        OutputLine("WeightAdjustment : %d", props->mLeaderWeightAdjustment);
+    }
+    /**
+     * @cli deviceprops (set)
+     * @code
+     * deviceprops battery 0 0 0 -5
+     * Done
+     * @endcode
+     * @code
+     * deviceprops
+     * PowerSupply      : battery
+     * IsBorderRouter   : no
+     * SupportsCcm      : no
+     * IsUnstable       : no
+     * WeightAdjustment : -5
+     * Done
+     * @endcode
+     * @cparam deviceprops @ca{powerSupply} @ca{isBr} @ca{supportsCcm} @ca{isUnstable} @ca{weightAdjustment}
+     * `powerSupply`: should be 'battery', 'external', 'external-stable', 'external-unstable'.
+     * @par
+     * Sets the device properties.
+     * @csa{leaderweight}
+     * @csa{leaderweight (set)}
+     * @sa #otThreadSetDeviceProperties
+     */
+    else
+    {
+        otDeviceProperties props;
+        bool               value;
+        uint8_t            index;
+
+        for (index = 0; index < OT_ARRAY_LENGTH(kPowerSupplyStrings); index++)
+        {
+            if (aArgs[0] == kPowerSupplyStrings[index])
+            {
+                props.mPowerSupply = static_cast<otPowerSupply>(index);
+                break;
+            }
+        }
+
+        VerifyOrExit(index < OT_ARRAY_LENGTH(kPowerSupplyStrings), error = OT_ERROR_INVALID_ARGS);
+
+        SuccessOrExit(error = aArgs[1].ParseAsBool(value));
+        props.mIsBorderRouter = value;
+
+        SuccessOrExit(error = aArgs[2].ParseAsBool(value));
+        props.mSupportsCcm = value;
+
+        SuccessOrExit(error = aArgs[3].ParseAsBool(value));
+        props.mIsUnstable = value;
+
+        SuccessOrExit(error = aArgs[4].ParseAsInt8(props.mLeaderWeightAdjustment));
+
+        VerifyOrExit(aArgs[5].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+        otThreadSetDeviceProperties(GetInstancePtr(), &props);
+    }
+
+exit:
+    return error;
+}
+
 #endif // OPENTHREAD_FTD
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
-void Interpreter::HandleLinkMetricsReport(const otIp6Address *       aAddress,
+void Interpreter::HandleLinkMetricsReport(const otIp6Address        *aAddress,
                                           const otLinkMetricsValues *aMetricsValues,
                                           uint8_t                    aStatus,
-                                          void *                     aContext)
+                                          void                      *aContext)
 {
     static_cast<Interpreter *>(aContext)->HandleLinkMetricsReport(aAddress, aMetricsValues, aStatus);
 }
 
 void Interpreter::PrintLinkMetricsValue(const otLinkMetricsValues *aMetricsValues)
 {
-    const char kLinkMetricsTypeCount[]   = "(Count/Summation)";
-    const char kLinkMetricsTypeAverage[] = "(Exponential Moving Average)";
+    static const char kLinkMetricsTypeAverage[] = "(Exponential Moving Average)";
 
     if (aMetricsValues->mMetrics.mPduCount)
     {
-        OutputLine(" - PDU Counter: %d %s", aMetricsValues->mPduCountValue, kLinkMetricsTypeCount);
+        OutputLine(" - PDU Counter: %lu (Count/Summation)", ToUlong(aMetricsValues->mPduCountValue));
     }
 
     if (aMetricsValues->mMetrics.mLqi)
     {
-        OutputLine(" - LQI: %d %s", aMetricsValues->mLqiValue, kLinkMetricsTypeAverage);
+        OutputLine(" - LQI: %u %s", aMetricsValues->mLqiValue, kLinkMetricsTypeAverage);
     }
 
     if (aMetricsValues->mMetrics.mLinkMargin)
     {
-        OutputLine(" - Margin: %d (dB) %s", aMetricsValues->mLinkMarginValue, kLinkMetricsTypeAverage);
+        OutputLine(" - Margin: %u (dB) %s", aMetricsValues->mLinkMarginValue, kLinkMetricsTypeAverage);
     }
 
     if (aMetricsValues->mMetrics.mRssi)
@@ -2488,7 +4527,7 @@
     }
 }
 
-void Interpreter::HandleLinkMetricsReport(const otIp6Address *       aAddress,
+void Interpreter::HandleLinkMetricsReport(const otIp6Address        *aAddress,
                                           const otLinkMetricsValues *aMetricsValues,
                                           uint8_t                    aStatus)
 {
@@ -2503,6 +4542,12 @@
     {
         OutputLine("Link Metrics Report, status: %s", LinkMetricsStatusToStr(aStatus));
     }
+
+    if (mLinkMetricsQueryInProgress)
+    {
+        mLinkMetricsQueryInProgress = false;
+        OutputResult(OT_ERROR_NONE);
+    }
 }
 
 void Interpreter::HandleLinkMetricsMgmtResponse(const otIp6Address *aAddress, uint8_t aStatus, void *aContext)
@@ -2519,15 +4564,15 @@
 }
 
 void Interpreter::HandleLinkMetricsEnhAckProbingIe(otShortAddress             aShortAddress,
-                                                   const otExtAddress *       aExtAddress,
+                                                   const otExtAddress        *aExtAddress,
                                                    const otLinkMetricsValues *aMetricsValues,
-                                                   void *                     aContext)
+                                                   void                      *aContext)
 {
     static_cast<Interpreter *>(aContext)->HandleLinkMetricsEnhAckProbingIe(aShortAddress, aExtAddress, aMetricsValues);
 }
 
 void Interpreter::HandleLinkMetricsEnhAckProbingIe(otShortAddress             aShortAddress,
-                                                   const otExtAddress *       aExtAddress,
+                                                   const otExtAddress        *aExtAddress,
                                                    const otLinkMetricsValues *aMetricsValues)
 {
     OutputFormat("Received Link Metrics data in Enh Ack from neighbor, short address:0x%02x , extended address:",
@@ -2577,34 +4622,95 @@
     if (aArgs[0] == "query")
     {
         otIp6Address  address;
+        bool          isSingle;
+        bool          blocking;
+        uint8_t       seriesId;
         otLinkMetrics linkMetrics;
 
         SuccessOrExit(error = aArgs[1].ParseAsIp6Address(address));
 
+        /**
+         * @cli linkmetrics query single
+         * @code
+         * linkmetrics query fe80:0:0:0:3092:f334:1455:1ad2 single qmr
+         * Done
+         * > Received Link Metrics Report from: fe80:0:0:0:3092:f334:1455:1ad2
+         * - LQI: 76 (Exponential Moving Average)
+         * - Margin: 82 (dB) (Exponential Moving Average)
+         * - RSSI: -18 (dBm) (Exponential Moving Average)
+         * @endcode
+         * @cparam linkmetrics query @ca{peer-ipaddr} single [@ca{pqmr}]
+         * - `peer-ipaddr`: Peer address.
+         * - [`p`, `q`, `m`, and `r`] map to #otLinkMetrics.
+         *   - `p`: Layer 2 Number of PDUs received.
+         *   - `q`: Layer 2 LQI.
+         *   - `m`: Link Margin.
+         *   - `r`: RSSI.
+         * @par
+         * Perform a Link Metrics query (Single Probe).
+         * @sa otLinkMetricsQuery
+         */
         if (aArgs[2] == "single")
         {
-            VerifyOrExit(!aArgs[3].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+            isSingle = true;
             SuccessOrExit(error = ParseLinkMetricsFlags(linkMetrics, aArgs[3]));
-            error = otLinkMetricsQuery(GetInstancePtr(), &address, /* aSeriesId */ 0, &linkMetrics,
-                                       &Interpreter::HandleLinkMetricsReport, this);
         }
+        /**
+         * @cli linkmetrics query forward
+         * @code
+         * linkmetrics query fe80:0:0:0:3092:f334:1455:1ad2 forward 1
+         * Done
+         * > Received Link Metrics Report from: fe80:0:0:0:3092:f334:1455:1ad2
+         * - PDU Counter: 2 (Count/Summation)
+         * - LQI: 76 (Exponential Moving Average)
+         * - Margin: 82 (dB) (Exponential Moving Average)
+         * - RSSI: -18 (dBm) (Exponential Moving Average)
+         * @endcode
+         * @cparam linkmetrics query @ca{peer-ipaddr} forward @ca{series-id}
+         * - `peer-ipaddr`: Peer address.
+         * - `series-id`: The Series ID.
+         * @par
+         * Perform a Link Metrics query (Forward Tracking Series).
+         * @sa otLinkMetricsQuery
+         */
         else if (aArgs[2] == "forward")
         {
-            uint8_t seriesId;
-
+            isSingle = false;
             SuccessOrExit(error = aArgs[3].ParseAsUint8(seriesId));
-            error = otLinkMetricsQuery(GetInstancePtr(), &address, seriesId, nullptr,
-                                       &Interpreter::HandleLinkMetricsReport, this);
         }
         else
         {
-            error = OT_ERROR_INVALID_ARGS;
+            ExitNow(error = OT_ERROR_INVALID_ARGS);
+        }
+
+        blocking = (aArgs[4] == "block");
+
+        SuccessOrExit(error = otLinkMetricsQuery(GetInstancePtr(), &address, isSingle ? 0 : seriesId,
+                                                 isSingle ? &linkMetrics : nullptr, HandleLinkMetricsReport, this));
+
+        if (blocking)
+        {
+            mLinkMetricsQueryInProgress = true;
+            error                       = OT_ERROR_PENDING;
         }
     }
     else if (aArgs[0] == "mgmt")
     {
         error = ProcessLinkMetricsMgmt(aArgs + 1);
     }
+    /**
+     * @cli linkmetrics probe
+     * @code
+     * linkmetrics probe fe80:0:0:0:3092:f334:1455:1ad2 1 10
+     * Done
+     * @endcode
+     * @cparam linkmetrics probe @ca{peer-ipaddr} @ca{series-id} @ca{length}
+     * - `peer-ipaddr`: Peer address.
+     * - `series-id`: The Series ID for which this Probe message targets.
+     * - `length`: The length of the Probe message. A valid range is [0, 64].
+     * @par api_copy
+     * #otLinkMetricsSendLinkProbe
+     */
     else if (aArgs[0] == "probe")
     {
         otIp6Address address;
@@ -2626,6 +4732,8 @@
 {
     otError error = OT_ERROR_NONE;
 
+    VerifyOrExit(!aFlags.IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
     memset(&aLinkMetrics, 0, sizeof(aLinkMetrics));
 
     for (const char *arg = aFlags.GetCString(); *arg != '\0'; arg++)
@@ -2668,6 +4776,32 @@
 
     memset(&seriesFlags, 0, sizeof(otLinkMetricsSeriesFlags));
 
+    /**
+     * @cli linkmetrics mgmt forward
+     * @code
+     * linkmetrics mgmt fe80:0:0:0:3092:f334:1455:1ad2 forward 1 dra pqmr
+     * Done
+     * > Received Link Metrics Management Response from: fe80:0:0:0:3092:f334:1455:1ad2
+     * Status: SUCCESS
+     * @endcode
+     * @cparam linkmetrics mgmt @ca{peer-ipaddr} forward @ca{series-id} [@ca{ldraX}][@ca{pqmr}]
+     * - `peer-ipaddr`: Peer address.
+     * - `series-id`: The Series ID.
+     * - [`l`, `d`, `r`, and `a`] map to #otLinkMetricsSeriesFlags. `X` represents none of the
+     *   `otLinkMetricsSeriesFlags`, and stops the accounting and removes the series.
+     *   - `l`: MLE Link Probe.
+     *   - `d`: MAC Data.
+     *   - `r`: MAC Data Request.
+     *   - `a`: MAC Ack.
+     *   - `X`: Can only be used without any other flags.
+     * - [`p`, `q`, `m`, and `r`] map to #otLinkMetricsValues.
+     *   - `p`: Layer 2 Number of PDUs received.
+     *   - `q`: Layer 2 LQI.
+     *   - `m`: Link Margin.
+     *   - `r`: RSSI.
+     * @par api_copy
+     * #otLinkMetricsConfigForwardTrackingSeries
+     */
     if (aArgs[1] == "forward")
     {
         uint8_t       seriesId;
@@ -2710,8 +4844,8 @@
 
         if (!clear)
         {
-            VerifyOrExit(!aArgs[4].IsEmpty() && aArgs[5].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
             SuccessOrExit(error = ParseLinkMetricsFlags(linkMetrics, aArgs[4]));
+            VerifyOrExit(aArgs[5].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
         }
 
         error = otLinkMetricsConfigForwardTrackingSeries(GetInstancePtr(), &address, seriesId, seriesFlags,
@@ -2722,17 +4856,59 @@
     {
         otLinkMetricsEnhAckFlags enhAckFlags;
         otLinkMetrics            linkMetrics;
-        otLinkMetrics *          pLinkMetrics = &linkMetrics;
+        otLinkMetrics           *pLinkMetrics = &linkMetrics;
 
+        /**
+         * @cli linkmetrics mgmt enhanced-ack clear
+         * @code
+         * linkmetrics mgmt fe80:0:0:0:3092:f334:1455:1ad2 enhanced-ack clear
+         * Done
+         * > Received Link Metrics Management Response from: fe80:0:0:0:3092:f334:1455:1ad2
+         * Status: Success
+         * @endcode
+         * @cparam linkmetrics mgmt @ca{peer-ipaddr} enhanced-ack clear
+         * `peer-ipaddr` should be the Link Local address of the neighboring device.
+         * @par
+         * Sends a Link Metrics Management Request to clear an Enhanced-ACK Based Probing.
+         * @sa otLinkMetricsConfigEnhAckProbing
+         */
         if (aArgs[2] == "clear")
         {
             enhAckFlags  = OT_LINK_METRICS_ENH_ACK_CLEAR;
             pLinkMetrics = nullptr;
         }
+        /**
+         * @cli linkmetrics mgmt enhanced-ack register
+         * @code
+         * linkmetrics mgmt fe80:0:0:0:3092:f334:1455:1ad2 enhanced-ack register qm
+         * Done
+         * > Received Link Metrics Management Response from: fe80:0:0:0:3092:f334:1455:1ad2
+         * Status: Success
+         * @endcode
+         * @code
+         * > linkmetrics mgmt fe80:0:0:0:3092:f334:1455:1ad2 enhanced-ack register qm r
+         * Done
+         * > Received Link Metrics Management Response from: fe80:0:0:0:3092:f334:1455:1ad2
+         * Status: Cannot support new series
+         * @endcode
+         * @cparam linkmetrics mgmt @ca{peer-ipaddr} enhanced-ack register [@ca{qmr}][@ca{r}]
+         * [`q`, `m`, and `r`] map to #otLinkMetricsValues. Per spec 4.11.3.4.4.6, you can
+         * only use a maximum of two options at once, for example `q`, or `qm`.
+         * - `q`: Layer 2 LQI.
+         * - `m`: Link Margin.
+         * - `r`: RSSI.
+         * .
+         * The additional `r` is optional and only used for reference devices. When this option
+         * is specified, Type/Average Enum of each Type Id Flags is set to reserved. This is
+         * used to verify that the Probing Subject correctly handles invalid Type Id Flags, and
+         * only available when `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is enabled.
+         * @par
+         * Sends a Link Metrics Management Request to register an Enhanced-ACK Based Probing.
+         * @sa otLinkMetricsConfigEnhAckProbing
+         */
         else if (aArgs[2] == "register")
         {
             enhAckFlags = OT_LINK_METRICS_ENH_ACK_REGISTER;
-            VerifyOrExit(!aArgs[3].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
             SuccessOrExit(error = ParseLinkMetricsFlags(linkMetrics, aArgs[3]));
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
             if (aArgs[4] == "r")
@@ -2785,7 +4961,7 @@
     return error;
 }
 
-void Interpreter::HandleLocateResult(void *              aContext,
+void Interpreter::HandleLocateResult(void               *aContext,
                                      otError             aError,
                                      const otIp6Address *aMeshLocalAddress,
                                      uint16_t            aRloc16)
@@ -2856,7 +5032,7 @@
 
     if (aArgs[0].IsEmpty())
     {
-        OutputLine("0x%04x", otThreadGetPskcRef(GetInstancePtr()));
+        OutputLine("0x%08lx", ToUlong(otThreadGetPskcRef(GetInstancePtr())));
     }
     else
     {
@@ -2941,7 +5117,7 @@
     return error;
 }
 
-void Interpreter::HandleMlrRegResult(void *              aContext,
+void Interpreter::HandleMlrRegResult(void               *aContext,
                                      otError             aError,
                                      uint8_t             aMlrStatus,
                                      const otIp6Address *aFailedAddresses,
@@ -3089,13 +5265,13 @@
 
     if (aMultiRadioInfo.mSupportsIeee802154)
     {
-        OutputFormat("15.4(%d)", aMultiRadioInfo.mIeee802154Info.mPreference);
+        OutputFormat("15.4(%u)", aMultiRadioInfo.mIeee802154Info.mPreference);
         isFirst = false;
     }
 
     if (aMultiRadioInfo.mSupportsTrelUdp6)
     {
-        OutputFormat("%sTREL(%d)", isFirst ? "" : ", ", aMultiRadioInfo.mTrelUdp6Info.mPreference);
+        OutputFormat("%sTREL(%u)", isFirst ? "" : ", ", aMultiRadioInfo.mTrelUdp6Info.mPreference);
     }
 
     OutputLine("]");
@@ -3117,11 +5293,11 @@
         if (isTable)
         {
             static const char *const kNeighborTableTitles[] = {
-                "Role", "RLOC16", "Age", "Avg RSSI", "Last RSSI", "R", "D", "N", "Extended MAC",
+                "Role", "RLOC16", "Age", "Avg RSSI", "Last RSSI", "R", "D", "N", "Extended MAC", "Version",
             };
 
             static const uint8_t kNeighborTableColumnWidths[] = {
-                6, 8, 5, 10, 11, 1, 1, 1, 18,
+                6, 8, 5, 10, 11, 1, 1, 1, 18, 9,
             };
 
             OutputTableHeader(kNeighborTableTitles, kNeighborTableColumnWidths);
@@ -3133,7 +5309,7 @@
             {
                 OutputFormat("| %3c  ", neighborInfo.mIsChild ? 'C' : 'R');
                 OutputFormat("| 0x%04x ", neighborInfo.mRloc16);
-                OutputFormat("| %3d ", neighborInfo.mAge);
+                OutputFormat("| %3lu ", ToUlong(neighborInfo.mAge));
                 OutputFormat("| %8d ", neighborInfo.mAverageRssi);
                 OutputFormat("| %9d ", neighborInfo.mLastRssi);
                 OutputFormat("|%1d", neighborInfo.mRxOnWhenIdle);
@@ -3141,7 +5317,7 @@
                 OutputFormat("|%1d", neighborInfo.mFullNetworkData);
                 OutputFormat("| ");
                 OutputExtAddress(neighborInfo.mExtAddress);
-                OutputLine(" |");
+                OutputLine(" | %7d |", neighborInfo.mVersion);
             }
             else
             {
@@ -3149,8 +5325,111 @@
             }
         }
 
-        OutputLine("");
+        OutputNewLine();
     }
+    else if (aArgs[0] == "linkquality")
+    {
+        static const char *const kLinkQualityTableTitles[] = {
+            "RLOC16", "Extended MAC", "Frame Error", "Msg Error", "Avg RSS", "Last RSS", "Age",
+        };
+
+        static const uint8_t kLinkQualityTableColumnWidths[] = {
+            8, 18, 13, 11, 9, 10, 7,
+        };
+
+        OutputTableHeader(kLinkQualityTableTitles, kLinkQualityTableColumnWidths);
+
+        while (otThreadGetNextNeighborInfo(GetInstancePtr(), &iterator, &neighborInfo) == OT_ERROR_NONE)
+        {
+            PercentageStringBuffer stringBuffer;
+
+            OutputFormat("| 0x%04x | ", neighborInfo.mRloc16);
+            OutputExtAddress(neighborInfo.mExtAddress);
+            OutputFormat(" | %9s %% ", PercentageToString(neighborInfo.mFrameErrorRate, stringBuffer));
+            OutputFormat("| %7s %% ", PercentageToString(neighborInfo.mMessageErrorRate, stringBuffer));
+            OutputFormat("| %7d ", neighborInfo.mAverageRssi);
+            OutputFormat("| %8d ", neighborInfo.mLastRssi);
+            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;
@@ -3234,10 +5513,7 @@
 }
 #endif // OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
 
-template <> otError Interpreter::Process<Cmd("netdata")>(Arg aArgs[])
-{
-    return mNetworkData.Process(aArgs);
-}
+template <> otError Interpreter::Process<Cmd("netdata")>(Arg aArgs[]) { return mNetworkData.Process(aArgs); }
 
 #if OPENTHREAD_FTD
 template <> otError Interpreter::Process<Cmd("networkidtimeout")>(Arg aArgs[])
@@ -3250,6 +5526,16 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli networkkey
+     * @code
+     * networkkey
+     * 00112233445566778899aabbccddeeff
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetNetworkKey
+     */
     if (aArgs[0].IsEmpty())
     {
         otNetworkKey networkKey;
@@ -3257,6 +5543,16 @@
         otThreadGetNetworkKey(GetInstancePtr(), &networkKey);
         OutputBytesLine(networkKey.m8);
     }
+    /**
+     * @cli networkkey (key)
+     * @code
+     * networkkey 00112233445566778899aabbccddeeff
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadSetNetworkKey
+     * @cparam networkkey @ca{key}
+     */
     else
     {
         otNetworkKey key;
@@ -3276,7 +5572,7 @@
 
     if (aArgs[0].IsEmpty())
     {
-        OutputLine("0x%04x", otThreadGetNetworkKeyRef(GetInstancePtr()));
+        OutputLine("0x%08lx", ToUlong(otThreadGetNetworkKeyRef(GetInstancePtr())));
     }
     else
     {
@@ -3291,21 +5587,31 @@
 }
 #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;
-
-    if (aArgs[0].IsEmpty())
-    {
-        OutputLine("%s", otThreadGetNetworkName(GetInstancePtr()));
-    }
-    else
-    {
-        SuccessOrExit(error = otThreadSetNetworkName(GetInstancePtr(), aArgs[0].GetCString()));
-    }
-
-exit:
-    return error;
+    /**
+     * @cli networkname (name)
+     * @code
+     * networkname OpenThread
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadSetNetworkName
+     * @cparam networkname @ca{name}
+     * @par
+     * Note: The current commissioning credential becomes stale after changing this value. Use `pskc` to reset.
+     */
+    return ProcessGetSet(aArgs, otThreadGetNetworkName, otThreadSetNetworkName);
 }
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
@@ -3313,6 +5619,21 @@
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli networktime
+     * @code
+     * networktime
+     * Network Time:     21084154us (synchronized)
+     * Time Sync Period: 100s
+     * XTAL Threshold:   300ppm
+     * Done
+     * @endcode
+     * @par
+     * Gets the Thread network time and the time sync parameters.
+     * @sa otNetworkTimeGet
+     * @sa otNetworkTimeGetSyncPeriod
+     * @sa otNetworkTimeGetXtalThreshold
+     */
     if (aArgs[0].IsEmpty())
     {
         uint64_t            time;
@@ -3320,7 +5641,9 @@
 
         networkTimeStatus = otNetworkTimeGet(GetInstancePtr(), &time);
 
-        OutputFormat("Network Time:     %luus", time);
+        OutputFormat("Network Time:     ");
+        OutputUint64(time);
+        OutputFormat("us");
 
         switch (networkTimeStatus)
         {
@@ -3340,9 +5663,23 @@
             break;
         }
 
-        OutputLine("Time Sync Period: %ds", otNetworkTimeGetSyncPeriod(GetInstancePtr()));
-        OutputLine("XTAL Threshold:   %dppm", otNetworkTimeGetXtalThreshold(GetInstancePtr()));
+        OutputLine("Time Sync Period: %us", otNetworkTimeGetSyncPeriod(GetInstancePtr()));
+        OutputLine("XTAL Threshold:   %uppm", otNetworkTimeGetXtalThreshold(GetInstancePtr()));
     }
+    /**
+     * @cli networktime (set)
+     * @code
+     * networktime 100 300
+     * Done
+     * @endcode
+     * @cparam networktime @ca{timesyncperiod} @ca{xtalthreshold}
+     * @par
+     * Sets the time sync parameters.
+     * *   `timesyncperiod`: The time synchronization period, in seconds.
+     * *   `xtalthreshold`: The XTAL accuracy threshold for a device to become Router-Capable device, in PPM.
+     * @sa otNetworkTimeSetSyncPeriod
+     * @sa otNetworkTimeSetXtalThreshold
+     */
     else
     {
         uint16_t period;
@@ -3359,14 +5696,347 @@
 }
 #endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 
-template <> otError Interpreter::Process<Cmd("panid")>(Arg aArgs[])
+#if OPENTHREAD_FTD
+template <> otError Interpreter::Process<Cmd("nexthop")>(Arg aArgs[])
+{
+    constexpr uint8_t  kRouterIdOffset = 10; // Bit offset of Router ID in RLOC16
+    constexpr uint16_t kInvalidRloc16  = 0xfffe;
+
+    otError  error = OT_ERROR_NONE;
+    uint16_t destRloc16;
+    uint16_t nextHopRloc16;
+    uint8_t  pathCost;
+
+    /**
+     * @cli nexthop
+     * @code
+     * nexthop
+     * | ID   |NxtHop| Cost |
+     * +------+------+------+
+     * |    9 |    9 |    1 |
+     * |   25 |   25 |    0 |
+     * |   30 |   30 |    1 |
+     * |   46 |    - |    - |
+     * |   50 |   30 |    3 |
+     * |   60 |   30 |    2 |
+     * Done
+     * @endcode
+     * @par
+     * Output table of allocated Router IDs and current next hop and path
+     * cost for each router.
+     * @sa otThreadGetNextHopAndPathCost
+     * @sa otThreadIsRouterIdAllocated
+     */
+    if (aArgs[0].IsEmpty())
+    {
+        static const char *const kNextHopTableTitles[] = {
+            "ID",
+            "NxtHop",
+            "Cost",
+        };
+
+        static const uint8_t kNextHopTableColumnWidths[] = {
+            6,
+            6,
+            6,
+        };
+
+        OutputTableHeader(kNextHopTableTitles, kNextHopTableColumnWidths);
+
+        for (uint8_t routerId = 0; routerId <= OT_NETWORK_MAX_ROUTER_ID; routerId++)
+        {
+            if (!otThreadIsRouterIdAllocated(GetInstancePtr(), routerId))
+            {
+                continue;
+            }
+
+            destRloc16 = routerId;
+            destRloc16 <<= kRouterIdOffset;
+
+            otThreadGetNextHopAndPathCost(GetInstancePtr(), destRloc16, &nextHopRloc16, &pathCost);
+
+            OutputFormat("| %4u | ", routerId);
+
+            if (nextHopRloc16 != kInvalidRloc16)
+            {
+                OutputLine("%4u | %4u |", nextHopRloc16 >> kRouterIdOffset, pathCost);
+            }
+            else
+            {
+                OutputLine("%4s | %4s |", "-", "-");
+            }
+        }
+    }
+    /**
+     * @cli nexthop (get)
+     * @code
+     * nexthop 0xc000
+     * 0xc000 cost:0
+     * Done
+     * @endcode
+     * @code
+     * nexthop 0x8001
+     * 0x2000 cost:3
+     * Done
+     * @endcode
+     * @cparam nexthop @ca{rloc16}
+     * @par api_copy
+     * #otThreadGetNextHopAndPathCost
+     */
+    else
+    {
+        SuccessOrExit(error = aArgs[0].ParseAsUint16(destRloc16));
+        otThreadGetNextHopAndPathCost(GetInstancePtr(), destRloc16, &nextHopRloc16, &pathCost);
+        OutputLine("0x%04x cost:%u", nextHopRloc16, pathCost);
+    }
+
+exit:
+    return error;
+}
+
+#if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE
+
+template <> otError Interpreter::Process<Cmd("meshdiag")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
 
+    /**
+     * @cli meshdiag topology
+     * @code
+     * meshdiag topology
+     * id:02 rloc16:0x0800 ext-addr:8aa57d2c603fe16c ver:4 - me - leader
+     *    3-links:{ 46 }
+     * id:46 rloc16:0xb800 ext-addr:fe109d277e0175cc ver:4
+     *    3-links:{ 02 51 57 }
+     * id:33 rloc16:0x8400 ext-addr:d2e511a146b9e54d ver:4
+     *    3-links:{ 51 57 }
+     * id:51 rloc16:0xcc00 ext-addr:9aab43ababf05352 ver:4
+     *    3-links:{ 33 57 }
+     *    2-links:{ 46 }
+     * id:57 rloc16:0xe400 ext-addr:dae9c4c0e9da55ff ver:4
+     *    3-links:{ 46 51 }
+     *    1-links:{ 33 }
+     * Done
+     * @endcode
+     * @par
+     * Discover network topology (list of routers and their connections).
+     * Parameters are optional and indicate additional items to discover. Can be added in any order.
+     * * `ip6-addrs` to discover the list of IPv6 addresses of every router.
+     * * `children` to discover the child table of every router.
+     * @par
+     * Information per router:
+     * * Router ID
+     * * RLOC16
+     * * Extended MAC address
+     * * Thread Version (if known)
+     * * Whether the router is this device is itself (`me`)
+     * * Whether the router is the parent of this device when device is a child (`parent`)
+     * * Whether the router is `leader`
+     * * Whether the router acts as a border router providing external connectivity (`br`)
+     * * List of routers to which this router has a link:
+     *   * `3-links`: Router IDs to which this router has a incoming link with link quality 3
+     *   * `2-links`: Router IDs to which this router has a incoming link with link quality 2
+     *   * `1-links`: Router IDs to which this router has a incoming link with link quality 1
+     *   * If a list if empty, it is omitted in the out.
+     * * If `ip6-addrs`, list of IPv6 addresses of the router
+     * * If `children`, list of all children of the router. Information per child:
+     *   * RLOC16
+     *   * Incoming Link Quality from perspective of parent to child (zero indicates unknown)
+     *   * Child Device mode (`r` rx-on-when-idle, `d` Full Thread Device, `n` Full Network Data, `-` no flags set)
+     *   * Whether the child is this device itself (`me`)
+     *   * Whether the child acts as a border router providing external connectivity (`br`)
+     * @cparam meshdiag topology [@ca{ip6-addrs}] [@ca{children}]
+     * @sa otMeshDiagDiscoverTopology
+     */
+    if (aArgs[0] == "topology")
+    {
+        otMeshDiagDiscoverConfig config;
+
+        config.mDiscoverIp6Addresses = false;
+        config.mDiscoverChildTable   = false;
+
+        aArgs++;
+
+        for (; !aArgs->IsEmpty(); aArgs++)
+        {
+            if (*aArgs == "ip6-addrs")
+            {
+                config.mDiscoverIp6Addresses = true;
+            }
+            else if (*aArgs == "children")
+            {
+                config.mDiscoverChildTable = true;
+            }
+            else
+            {
+                ExitNow(error = OT_ERROR_INVALID_ARGS);
+            }
+        }
+
+        SuccessOrExit(error = otMeshDiagDiscoverTopology(GetInstancePtr(), &config, HandleMeshDiagDiscoverDone, this));
+        error = OT_ERROR_PENDING;
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_COMMAND;
+    }
+
+exit:
+    return error;
+}
+
+void Interpreter::HandleMeshDiagDiscoverDone(otError aError, otMeshDiagRouterInfo *aRouterInfo, void *aContext)
+{
+    reinterpret_cast<Interpreter *>(aContext)->HandleMeshDiagDiscoverDone(aError, aRouterInfo);
+}
+
+void Interpreter::HandleMeshDiagDiscoverDone(otError aError, otMeshDiagRouterInfo *aRouterInfo)
+{
+    VerifyOrExit(aRouterInfo != nullptr);
+
+    OutputFormat("id:%02u rloc16:0x%04x ext-addr:", aRouterInfo->mRouterId, aRouterInfo->mRloc16);
+    OutputExtAddress(aRouterInfo->mExtAddress);
+
+    if (aRouterInfo->mVersion != OT_MESH_DIAG_VERSION_UNKNOWN)
+    {
+        OutputFormat(" ver:%u", aRouterInfo->mVersion);
+    }
+
+    if (aRouterInfo->mIsThisDevice)
+    {
+        OutputFormat(" - me");
+    }
+
+    if (aRouterInfo->mIsThisDeviceParent)
+    {
+        OutputFormat(" - parent");
+    }
+
+    if (aRouterInfo->mIsLeader)
+    {
+        OutputFormat(" - leader");
+    }
+
+    if (aRouterInfo->mIsBorderRouter)
+    {
+        OutputFormat(" - br");
+    }
+
+    OutputNewLine();
+
+    for (uint8_t linkQuality = 3; linkQuality > 0; linkQuality--)
+    {
+        bool hasLinkQuality = false;
+
+        for (uint8_t entryQuality : aRouterInfo->mLinkQualities)
+        {
+            if (entryQuality == linkQuality)
+            {
+                hasLinkQuality = true;
+                break;
+            }
+        }
+
+        if (hasLinkQuality)
+        {
+            OutputFormat(kIndentSize, "%u-links:{ ", linkQuality);
+
+            for (uint8_t id = 0; id < OT_ARRAY_LENGTH(aRouterInfo->mLinkQualities); id++)
+            {
+                if (aRouterInfo->mLinkQualities[id] == linkQuality)
+                {
+                    OutputFormat("%02u ", id);
+                }
+            }
+
+            OutputLine("}");
+        }
+    }
+
+    if (aRouterInfo->mIp6AddrIterator != nullptr)
+    {
+        otIp6Address ip6Address;
+
+        OutputLine(kIndentSize, "ip6-addrs:");
+
+        while (otMeshDiagGetNextIp6Address(aRouterInfo->mIp6AddrIterator, &ip6Address) == OT_ERROR_NONE)
+        {
+            OutputSpaces(kIndentSize * 2);
+            OutputIp6AddressLine(ip6Address);
+        }
+    }
+
+    if (aRouterInfo->mChildIterator != nullptr)
+    {
+        otMeshDiagChildInfo childInfo;
+        char                linkModeString[kLinkModeStringSize];
+        bool                isFirst = true;
+
+        while (otMeshDiagGetNextChildInfo(aRouterInfo->mChildIterator, &childInfo) == OT_ERROR_NONE)
+        {
+            if (isFirst)
+            {
+                OutputLine(kIndentSize, "children:");
+                isFirst = false;
+            }
+
+            OutputFormat(kIndentSize * 2, "rloc16:0x%04x lq:%u, mode:%s", childInfo.mRloc16, childInfo.mLinkQuality,
+                         LinkModeToString(childInfo.mMode, linkModeString));
+
+            if (childInfo.mIsThisDevice)
+            {
+                OutputFormat(" - me");
+            }
+
+            if (childInfo.mIsBorderRouter)
+            {
+                OutputFormat(" - br");
+            }
+
+            OutputNewLine();
+        }
+
+        if (isFirst)
+        {
+            OutputLine(kIndentSize, "children: none");
+        }
+    }
+
+exit:
+    OutputResult(aError);
+}
+
+#endif // OPENTHREAD_CONFIG_MESH_DIAG_ENABLE
+
+#endif // OPENTHREAD_FTD
+
+template <> otError Interpreter::Process<Cmd("panid")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+    /**
+     * @cli panid
+     * @code
+     * panid
+     * 0xdead
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otLinkGetPanId
+     */
     if (aArgs[0].IsEmpty())
     {
         OutputLine("0x%04x", otLinkGetPanId(GetInstancePtr()));
     }
+    /**
+     * @cli panid (panid)
+     * @code
+     * panid 0xdead
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otLinkSetPanId
+     * @cparam panid @ca{panid}
+     */
     else
     {
         error = ProcessSet(aArgs, otLinkSetPanId);
@@ -3377,24 +6047,88 @@
 
 template <> otError Interpreter::Process<Cmd("parent")>(Arg aArgs[])
 {
-    OT_UNUSED_VARIABLE(aArgs);
+    otError error = OT_ERROR_NONE;
+    /**
+     * @cli parent
+     * @code
+     * parent
+     * Ext Addr: be1857c6c21dce55
+     * Rloc: 5c00
+     * Link Quality In: 3
+     * Link Quality Out: 3
+     * Age: 20
+     * Version: 4
+     * Done
+     * @endcode
+     * @sa otThreadGetParentInfo
+     * @par
+     * Get the diagnostic information for a Thread Router as parent.
+     * @par
+     * When operating as a Thread Router when OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE is enabled, this command
+     * will return the cached information from when the device was previously attached as a Thread Child. Returning
+     * cached information is necessary to support the Thread Test Harness - Test Scenario 8.2.x requests the former
+     * parent (i.e. %Joiner Router's) MAC address even if the device has already promoted to a router.
+     * @par
+     * Note: When OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE is enabled, this command will return two extra lines with
+     * information relevant for CSL Receiver operation.
+     */
+    if (aArgs[0].IsEmpty())
+    {
+        otRouterInfo parentInfo;
 
-    otError      error = OT_ERROR_NONE;
-    otRouterInfo parentInfo;
-
-    SuccessOrExit(error = otThreadGetParentInfo(GetInstancePtr(), &parentInfo));
-    OutputFormat("Ext Addr: ");
-    OutputExtAddressLine(parentInfo.mExtAddress);
-    OutputLine("Rloc: %x", parentInfo.mRloc16);
-    OutputLine("Link Quality In: %d", parentInfo.mLinkQualityIn);
-    OutputLine("Link Quality Out: %d", parentInfo.mLinkQualityOut);
-    OutputLine("Age: %d", parentInfo.mAge);
+        SuccessOrExit(error = otThreadGetParentInfo(GetInstancePtr(), &parentInfo));
+        OutputFormat("Ext Addr: ");
+        OutputExtAddressLine(parentInfo.mExtAddress);
+        OutputLine("Rloc: %x", parentInfo.mRloc16);
+        OutputLine("Link Quality In: %u", parentInfo.mLinkQualityIn);
+        OutputLine("Link Quality Out: %u", parentInfo.mLinkQualityOut);
+        OutputLine("Age: %lu", ToUlong(parentInfo.mAge));
+        OutputLine("Version: %u", parentInfo.mVersion);
+#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+        OutputLine("CSL clock accuracy: %u", parentInfo.mCslClockAccuracy);
+        OutputLine("CSL uncertainty: %u", parentInfo.mCslUncertainty);
+#endif
+    }
+    /**
+     * @cli parent search
+     * @code
+     * parent search
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadSearchForBetterParent
+     */
+    else if (aArgs[0] == "search")
+    {
+        error = otThreadSearchForBetterParent(GetInstancePtr());
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_ARGS;
+    }
 
 exit:
     return error;
 }
 
 #if OPENTHREAD_FTD
+/**
+ * @cli parentpriority (get,set)
+ * @code
+ * parentpriority
+ * 1
+ * Done
+ * @endcode
+ * @code
+ * parentpriority 1
+ * Done
+ * @endcode
+ * @cparam parentpriority [@ca{parentpriority}]
+ * @par
+ * Gets or sets the assigned parent priority value: 1, 0, -1 or -2. -2 means not assigned.
+ * @sa otThreadGetParentPriority
+ * @sa otThreadSetParentPriority
+ */
 template <> otError Interpreter::Process<Cmd("parentpriority")>(Arg aArgs[])
 {
     return ProcessGetSet(aArgs, otThreadGetParentPriority, otThreadSetParentPriority);
@@ -3411,7 +6145,7 @@
     if (aArgs[0].IsEmpty())
     {
         otThreadGetRouterIdRange(GetInstancePtr(), &minRouterId, &maxRouterId);
-        OutputLine("%d %d", minRouterId, maxRouterId);
+        OutputLine("%u %u", minRouterId, maxRouterId);
     }
     else
     {
@@ -3437,7 +6171,7 @@
 {
     OutputFormat("%u bytes from ", static_cast<uint16_t>(aReply->mSize + sizeof(otIcmp6Header)));
     OutputIp6Address(aReply->mSenderAddress);
-    OutputLine(": icmp_seq=%d hlim=%d time=%dms", aReply->mSequenceNumber, aReply->mHopLimit, aReply->mRoundTripTime);
+    OutputLine(": icmp_seq=%u hlim=%u time=%ums", aReply->mSequenceNumber, aReply->mHopLimit, aReply->mRoundTripTime);
 }
 
 void Interpreter::HandlePingStatistics(const otPingSenderStatistics *aStatistics, void *aContext)
@@ -3454,17 +6188,21 @@
     {
         uint32_t packetLossRate =
             1000 * (aStatistics->mSentCount - aStatistics->mReceivedCount) / aStatistics->mSentCount;
-        OutputFormat(" Packet loss = %u.%u%%.", packetLossRate / 10, packetLossRate % 10);
+
+        OutputFormat(" Packet loss = %lu.%u%%.", ToUlong(packetLossRate / 10),
+                     static_cast<uint16_t>(packetLossRate % 10));
     }
 
     if (aStatistics->mReceivedCount != 0)
     {
         uint32_t avgRoundTripTime = 1000 * aStatistics->mTotalRoundTripTime / aStatistics->mReceivedCount;
+
         OutputFormat(" Round-trip min/avg/max = %u/%u.%u/%u ms.", aStatistics->mMinRoundTripTime,
-                     avgRoundTripTime / 1000, avgRoundTripTime % 1000, aStatistics->mMaxRoundTripTime);
+                     static_cast<uint16_t>(avgRoundTripTime / 1000), static_cast<uint16_t>(avgRoundTripTime % 1000),
+                     aStatistics->mMaxRoundTripTime);
     }
 
-    OutputLine("");
+    OutputNewLine();
 
     if (!mPingIsAsync)
     {
@@ -3472,12 +6210,55 @@
     }
 }
 
+/**
+ * @cli ping
+ * @code
+ * ping fd00:db8:0:0:76b:6a05:3ae9:a61a
+ * 16 bytes from fd00:db8:0:0:76b:6a05:3ae9:a61a: icmp_seq=5 hlim=64 time=0ms
+ * 1 packets transmitted, 1 packets received. Packet loss = 0.0%. Round-trip min/avg/max = 0/0.0/0 ms.
+ * Done
+ * @endcode
+ * @code
+ * ping -I fd00:db8:0:0:76b:6a05:3ae9:a61a ff02::1 100 1 1 1
+ * 108 bytes from fd00:db8:0:0:f605:fb4b:d429:d59a: icmp_seq=4 hlim=64 time=7ms
+ * 1 packets transmitted, 1 packets received. Round-trip min/avg/max = 7/7.0/7 ms.
+ * Done
+ * @endcode
+ * @code
+ * ping 172.17.0.1
+ * Pinging synthesized IPv6 address: fdde:ad00:beef:2:0:0:ac11:1
+ * 16 bytes from fdde:ad00:beef:2:0:0:ac11:1: icmp_seq=5 hlim=64 time=0ms
+ * 1 packets transmitted, 1 packets received. Packet loss = 0.0%. Round-trip min/avg/max = 0/0.0/0 ms.
+ * Done
+ * @endcode
+ * @cparam ping [@ca{async}] [@ca{-I source}] @ca{ipaddrc} [@ca{size}] [@ca{count}] <!--
+ * -->          [@ca{interval}] [@ca{hoplimit}] [@ca{timeout}]
+ * @par
+ * Send an ICMPv6 Echo Request.
+ * @par
+ * The address 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 preferred NAT64 prefix is unavailable.
+ * @sa otPingSenderPing
+ */
 template <> otError Interpreter::Process<Cmd("ping")>(Arg aArgs[])
 {
     otError            error = OT_ERROR_NONE;
     otPingSenderConfig config;
     bool               async = false;
+    bool               nat64SynthesizedAddress;
 
+    /**
+     * @cli ping stop
+     * @code
+     * ping stop
+     * Done
+     * @endcode
+     * @par
+     * Stop sending ICMPv6 Echo Requests.
+     * @sa otPingSenderStop
+     */
     if (aArgs[0] == "stop")
     {
         otPingSenderStop(GetInstancePtr());
@@ -3516,7 +6297,12 @@
         aArgs += 2;
     }
 
-    SuccessOrExit(error = aArgs[0].ParseAsIp6Address(config.mDestination));
+    SuccessOrExit(error = ParseToIp6Address(GetInstancePtr(), aArgs[0], config.mDestination, nat64SynthesizedAddress));
+    if (nat64SynthesizedAddress)
+    {
+        OutputFormat("Pinging synthesized IPv6 address: ");
+        OutputIp6AddressLine(config.mDestination);
+    }
 
     if (!aArgs[1].IsEmpty())
     {
@@ -3569,6 +6355,39 @@
 
 #endif // OPENTHREAD_CONFIG_PING_SENDER_ENABLE
 
+/**
+ * @cli platform
+ * @code
+ * platform
+ * NRF52840
+ * Done
+ * @endcode
+ * @par
+ * Print the current platform
+ */
+template <> otError Interpreter::Process<Cmd("platform")>(Arg aArgs[])
+{
+    OT_UNUSED_VARIABLE(aArgs);
+    OutputLine("%s", OPENTHREAD_CONFIG_PLATFORM_INFO);
+    return OT_ERROR_NONE;
+}
+
+/**
+ * @cli pollperiod (get,set)
+ * @code
+ * pollperiod
+ * 0
+ * Done
+ * @endcode
+ * @code
+ * pollperiod 10
+ * Done
+ * @endcode
+ * @sa otLinkGetPollPeriod
+ * @sa otLinkSetPollPeriod
+ * @par
+ * Get or set the customized data poll period of sleepy end device (milliseconds). Only for certification test.
+ */
 template <> otError Interpreter::Process<Cmd("pollperiod")>(Arg aArgs[])
 {
     return ProcessGetSet(aArgs, otLinkGetPollPeriod, otLinkSetPollPeriod);
@@ -3615,7 +6434,7 @@
 {
     OT_UNUSED_VARIABLE(aIsTx);
 
-    OutputLine("");
+    OutputNewLine();
 
     for (size_t i = 0; i < 44; i++)
     {
@@ -3629,7 +6448,7 @@
         OutputFormat("=");
     }
 
-    OutputLine("");
+    OutputNewLine();
 
     for (size_t i = 0; i < aFrame->mLength; i += 16)
     {
@@ -3676,7 +6495,7 @@
         OutputFormat("-");
     }
 
-    OutputLine("");
+    OutputNewLine();
 }
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
@@ -3740,6 +6559,9 @@
                     aConfig.mDp = true;
                     break;
 #endif
+                case '-':
+                    break;
+
                 default:
                     ExitNow(error = OT_ERROR_INVALID_ARGS);
                 }
@@ -3901,11 +6723,21 @@
     const char *version = otPlatRadioGetVersionString(GetInstancePtr());
 
     VerifyOrExit(version != otGetVersionString(), error = OT_ERROR_NOT_IMPLEMENTED);
-
+    /**
+     * @cli rcp version
+     * @code
+     * rcp version
+     * OPENTHREAD/20191113-00825-g82053cc9d-dirty; SIMULATION; Jun  4 2020 17:53:16
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otPlatRadioGetVersionString
+     */
     if (aArgs[0] == "version")
     {
         OutputLine("%s", version);
     }
+
     else
     {
         error = OT_ERROR_INVALID_ARGS;
@@ -3914,7 +6746,16 @@
 exit:
     return error;
 }
-
+/**
+ * @cli region
+ * @code
+ * region
+ * US
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otPlatRadioGetRegion
+ */
 template <> otError Interpreter::Process<Cmd("region")>(Arg aArgs[])
 {
     otError  error = OT_ERROR_NONE;
@@ -3925,7 +6766,19 @@
         SuccessOrExit(error = otPlatRadioGetRegion(GetInstancePtr(), &regionCode));
         OutputLine("%c%c", regionCode >> 8, regionCode & 0xff);
     }
+    /**
+     * @cli region (set)
+     * @code
+     * region US
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otPlatRadioSetRegion
+     * @par
+     * Changing this can affect the transmit power limit.
+     */
     else
+
     {
         VerifyOrExit(aArgs[0].GetLength() == 2, error = OT_ERROR_INVALID_ARGS);
 
@@ -3939,13 +6792,34 @@
 }
 
 #if OPENTHREAD_FTD
+/**
+ * @cli releaserouterid (routerid)
+ * @code
+ * releaserouterid 16
+ * Done
+ * @endcode
+ * @cparam releaserouterid [@ca{routerid}]
+ * @par api_copy
+ * #otThreadReleaseRouterId
+ */
 template <> otError Interpreter::Process<Cmd("releaserouterid")>(Arg aArgs[])
+
 {
     return ProcessSet(aArgs, otThreadReleaseRouterId);
 }
 #endif
-
+/**
+ * @cli rloc16
+ * @code
+ * rloc16
+ * 0xdead
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otThreadGetRloc16
+ */
 template <> otError Interpreter::Process<Cmd("rloc16")>(Arg aArgs[])
+
 {
     OT_UNUSED_VARIABLE(aArgs);
 
@@ -3968,21 +6842,31 @@
     {
         otRoutePreference preference;
 
-        if (*aArgs == "s")
-        {
-            aConfig.mStable = true;
-        }
-        else if (*aArgs == "n")
-        {
-            aConfig.mNat64 = true;
-        }
-        else if (ParsePreference(*aArgs, preference) == OT_ERROR_NONE)
+        if (ParsePreference(*aArgs, preference) == OT_ERROR_NONE)
         {
             aConfig.mPreference = preference;
         }
         else
         {
-            ExitNow(error = OT_ERROR_INVALID_ARGS);
+            for (char *arg = aArgs->GetCString(); *arg != '\0'; arg++)
+            {
+                switch (*arg)
+                {
+                case 's':
+                    aConfig.mStable = true;
+                    break;
+
+                case 'n':
+                    aConfig.mNat64 = true;
+                    break;
+
+                case '-':
+                    break;
+
+                default:
+                    ExitNow(error = OT_ERROR_INVALID_ARGS);
+                }
+            }
         }
     }
 
@@ -3993,7 +6877,17 @@
 template <> otError Interpreter::Process<Cmd("route")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
-
+    /**
+     * @cli route
+     * @code
+     * route
+     * 2001:dead:beef:cafe::/64 s med
+     * Done
+     * @endcode
+     * @sa otBorderRouterGetNextRoute
+     * @par
+     * Get the external route list in the local Network Data.
+     */
     if (aArgs[0].IsEmpty())
     {
         otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
@@ -4004,6 +6898,23 @@
             mNetworkData.OutputRoute(config);
         }
     }
+    /**
+     * @cli route add
+     * @code
+     * route add 2001:dead:beef:cafe::/64 s med
+     * Done
+     * @endcode
+     * @par
+     * For parameters, use:
+     * *    s: Stable flag
+     * *    n: NAT64 flag
+     * *    prf: Default Router Preference, [high, med, low].
+     * @cparam route add @ca{prefix} [@ca{sn}] [@ca{high}|@ca{med}|@ca{low}]
+     * @par api_copy
+     * #otExternalRouteConfig
+     * @par
+     * Add a valid external route to the Network Data.
+     */
     else if (aArgs[0] == "add")
     {
         otExternalRouteConfig config;
@@ -4011,6 +6922,16 @@
         SuccessOrExit(error = ParseRoute(aArgs + 1, config));
         error = otBorderRouterAddRoute(GetInstancePtr(), &config);
     }
+    /**
+     * @cli route remove
+     * @code
+     * route remove 2001:dead:beef:cafe::/64
+     * Done
+     * @endcode
+     * @cparam route remove [@ca{prefix}]
+     * @par api_copy
+     * #otBorderRouterRemoveRoute
+     */
     else if (aArgs[0] == "remove")
     {
         otIp6Prefix prefix;
@@ -4035,9 +6956,32 @@
     otRouterInfo routerInfo;
     uint16_t     routerId;
     bool         isTable;
-
+    /**
+     * @cli router table
+     * @code
+     * router table
+     * | ID | RLOC16 | Next Hop | Path Cost | LQ In | LQ Out | Age | Extended MAC     | Link |
+     * +----+--------+----------+-----------+-------+--------+-----+------------------+------+
+     * | 22 | 0x5800 |       63 |         0 |     0 |      0 |   0 | 0aeb8196c9f61658 |    0 |
+     * | 49 | 0xc400 |       63 |         0 |     3 |      3 |   0 | faa1c03908e2dbf2 |    1 |
+     * Done
+     * @endcode
+     * @sa otThreadGetRouterInfo
+     * @par
+     * Prints a list of routers in a table format.
+     */
     isTable = (aArgs[0] == "table");
-
+    /**
+     * @cli router list
+     * @code
+     * router list
+     * 8 24 50
+     * Done
+     * @endcode
+     * @sa otThreadGetRouterInfo
+     * @par
+     * List allocated Router IDs.
+     */
     if (isTable || (aArgs[0] == "list"))
     {
         uint8_t maxRouterId;
@@ -4066,27 +7010,63 @@
 
             if (isTable)
             {
-                OutputFormat("| %2d ", routerInfo.mRouterId);
+                OutputFormat("| %2u ", routerInfo.mRouterId);
                 OutputFormat("| 0x%04x ", routerInfo.mRloc16);
-                OutputFormat("| %8d ", routerInfo.mNextHop);
-                OutputFormat("| %9d ", routerInfo.mPathCost);
-                OutputFormat("| %5d ", routerInfo.mLinkQualityIn);
-                OutputFormat("| %6d ", routerInfo.mLinkQualityOut);
-                OutputFormat("| %3d ", routerInfo.mAge);
+                OutputFormat("| %8u ", routerInfo.mNextHop);
+                OutputFormat("| %9u ", routerInfo.mPathCost);
+                OutputFormat("| %5u ", routerInfo.mLinkQualityIn);
+                OutputFormat("| %6u ", routerInfo.mLinkQualityOut);
+                OutputFormat("| %3u ", routerInfo.mAge);
                 OutputFormat("| ");
                 OutputExtAddress(routerInfo.mExtAddress);
                 OutputLine(" | %4d |", routerInfo.mLinkEstablished);
             }
             else
             {
-                OutputFormat("%d ", i);
+                OutputFormat("%u ", i);
             }
         }
 
-        OutputLine("");
+        OutputNewLine();
         ExitNow();
     }
-
+    /**
+     * @cli router (id)
+     * @code
+     * router 50
+     * Alloc: 1
+     * Router ID: 50
+     * Rloc: c800
+     * Next Hop: c800
+     * Link: 1
+     * Ext Addr: e2b3540590b0fd87
+     * Cost: 0
+     * Link Quality In: 3
+     * Link Quality Out: 3
+     * Age: 3
+     * Done
+     * @endcode
+     * @code
+     * router 0xc800
+     * Alloc: 1
+     * Router ID: 50
+     * Rloc: c800
+     * Next Hop: c800
+     * Link: 1
+     * Ext Addr: e2b3540590b0fd87
+     * Cost: 0
+     * Link Quality In: 3
+     * Link Quality Out: 3
+     * Age: 7
+     * Done
+     * @endcode
+     * @cparam router [@ca{id}]
+     * @par api_copy
+     * #otThreadGetRouterInfo
+     * @par
+     * Print diagnostic information for a Thread Router. The id may be a Router ID or
+     * an RLOC16.
+     */
     SuccessOrExit(error = aArgs[0].ParseAsUint16(routerId));
     SuccessOrExit(error = otThreadGetRouterInfo(GetInstancePtr(), routerId, &routerInfo));
 
@@ -4094,7 +7074,7 @@
 
     if (routerInfo.mAllocated)
     {
-        OutputLine("Router ID: %d", routerInfo.mRouterId);
+        OutputLine("Router ID: %u", routerInfo.mRouterId);
         OutputLine("Rloc: %04x", routerInfo.mRloc16);
         OutputLine("Next Hop: %04x", static_cast<uint16_t>(routerInfo.mNextHop) << 10);
         OutputLine("Link: %d", routerInfo.mLinkEstablished);
@@ -4103,17 +7083,31 @@
         {
             OutputFormat("Ext Addr: ");
             OutputExtAddressLine(routerInfo.mExtAddress);
-            OutputLine("Cost: %d", routerInfo.mPathCost);
-            OutputLine("Link Quality In: %d", routerInfo.mLinkQualityIn);
-            OutputLine("Link Quality Out: %d", routerInfo.mLinkQualityOut);
-            OutputLine("Age: %d", routerInfo.mAge);
+            OutputLine("Cost: %u", routerInfo.mPathCost);
+            OutputLine("Link Quality In: %u", routerInfo.mLinkQualityIn);
+            OutputLine("Link Quality Out: %u", routerInfo.mLinkQualityOut);
+            OutputLine("Age: %u", routerInfo.mAge);
         }
     }
 
 exit:
     return error;
 }
-
+/**
+ * @cli routerdowngradethreshold (get,set)
+ * @code routerdowngradethreshold
+ * 23
+ * Done
+ * @endcode
+ * @code routerdowngradethreshold 23
+ * Done
+ * @endcode
+ * @cparam routerdowngradethreshold [@ca{threshold}]
+ * @par
+ * Gets or sets the ROUTER_DOWNGRADE_THRESHOLD value.
+ * @sa otThreadGetRouterDowngradeThreshold
+ * @sa otThreadSetRouterDowngradeThreshold
+ */
 template <> otError Interpreter::Process<Cmd("routerdowngradethreshold")>(Arg aArgs[])
 {
     return ProcessGetSet(aArgs, otThreadGetRouterDowngradeThreshold, otThreadSetRouterDowngradeThreshold);
@@ -4122,11 +7116,36 @@
 template <> otError Interpreter::Process<Cmd("routereligible")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
-
+    /**
+     * @cli routereligible
+     * @code
+     * routereligible
+     * Enabled
+     * Done
+     * @endcode
+     * @sa otThreadIsRouterEligible
+     * @par
+     * Indicates whether the router role is enabled or disabled.
+     */
     if (aArgs[0].IsEmpty())
     {
         OutputEnabledDisabledStatus(otThreadIsRouterEligible(GetInstancePtr()));
     }
+    /**
+     * @cli routereligible (enable,disable)
+     * @code
+     * routereligible enable
+     * Done
+     * @endcode
+     * @code
+     * routereligible disable
+     * Done
+     * @endcode
+     * @cparam routereligible [@ca{enable|disable}]
+     * @sa otThreadSetRouterEligible
+     * @par
+     * Enables or disables the router role.
+     */
     else
     {
         bool enable;
@@ -4138,16 +7157,69 @@
 exit:
     return error;
 }
-
+/**
+ * @cli routerselectionjitter
+ * @code
+ * routerselectionjitter
+ * 120
+ * Done
+ * @endcode
+ * @code
+ * routerselectionjitter 120
+ * Done
+ * @endcode
+ * @cparam routerselectionjitter [@ca{jitter}]
+ * @par
+ * Gets or sets the ROUTER_SELECTION_JITTER value.
+ * @sa otThreadGetRouterSelectionJitter
+ * @sa otThreadSetRouterSelectionJitter
+ */
 template <> otError Interpreter::Process<Cmd("routerselectionjitter")>(Arg aArgs[])
 {
     return ProcessGetSet(aArgs, otThreadGetRouterSelectionJitter, otThreadSetRouterSelectionJitter);
 }
-
+/**
+ * @cli routerupgradethreshold (get,set)
+ * @code
+ * routerupgradethreshold
+ * 16
+ * Done
+ * @endcode
+ * @code
+ * routerupgradethreshold 16
+ * Done
+ * @endcode
+ * @cparam routerupgradethreshold [@ca{threshold}]
+ * @par
+ * Gets or sets the ROUTER_UPGRADE_THRESHOLD value.
+ * @sa otThreadGetRouterUpgradeThreshold
+ * @sa otThreadSetRouterUpgradeThreshold
+ */
 template <> otError Interpreter::Process<Cmd("routerupgradethreshold")>(Arg aArgs[])
 {
     return ProcessGetSet(aArgs, otThreadGetRouterUpgradeThreshold, otThreadSetRouterUpgradeThreshold);
 }
+/**
+ * @cli childrouterlinks (get,set)
+ * @code
+ * childrouterlinks
+ * 16
+ * Done
+ * @endcode
+ * @code
+ * childrouterlinks 16
+ * Done
+ * @endcode
+ * @cparam childrouterlinks [@ca{links}]
+ * @par
+ * Gets or sets the MLE_CHILD_ROUTER_LINKS value.
+ * @sa otThreadGetChildRouterLinks
+ * @sa otThreadSetChildRouterLinks
+ */
+template <> otError Interpreter::Process<Cmd("childrouterlinks")>(Arg aArgs[])
+{
+    return ProcessGetSet(aArgs, otThreadGetChildRouterLinks, otThreadSetChildRouterLinks);
+}
 #endif // OPENTHREAD_FTD
 
 template <> otError Interpreter::Process<Cmd("scan")>(Arg aArgs[])
@@ -4228,9 +7300,9 @@
 
     OutputFormat("| %04x | ", aResult->mPanId);
     OutputExtAddress(aResult->mExtAddress);
-    OutputFormat(" | %2d ", aResult->mChannel);
+    OutputFormat(" | %2u ", aResult->mChannel);
     OutputFormat("| %3d ", aResult->mRssi);
-    OutputLine("| %3d |", aResult->mLqi);
+    OutputLine("| %3u |", aResult->mLqi);
 
 exit:
     return;
@@ -4249,7 +7321,7 @@
         ExitNow();
     }
 
-    OutputLine("| %2d | %4d |", aResult->mChannel, aResult->mMaxRssi);
+    OutputLine("| %2u | %4d |", aResult->mChannel, aResult->mMaxRssi);
 
 exit:
     return;
@@ -4320,8 +7392,8 @@
     {
         // Some Embedded C libraries do not support printing of 64-bit unsigned integers.
         // To simplify, unix epoch time and era number are printed separately.
-        OutputLine("SNTP response - Unix time: %u (era: %u)", static_cast<uint32_t>(aTime),
-                   static_cast<uint32_t>(aTime >> 32));
+        OutputLine("SNTP response - Unix time: %lu (era: %lu)", ToUlong(static_cast<uint32_t>(aTime)),
+                   ToUlong(static_cast<uint32_t>(aTime >> 32)));
     }
     else
     {
@@ -4428,10 +7500,7 @@
     return error;
 }
 
-template <> otError Interpreter::Process<Cmd("dataset")>(Arg aArgs[])
-{
-    return mDataset.Process(aArgs);
-}
+template <> otError Interpreter::Process<Cmd("dataset")>(Arg aArgs[]) { return mDataset.Process(aArgs); }
 
 template <> otError Interpreter::Process<Cmd("txpower")>(Arg aArgs[])
 {
@@ -4454,16 +7523,10 @@
 }
 
 #if OPENTHREAD_CONFIG_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_TCP_ENABLE
-template <> otError Interpreter::Process<Cmd("tcp")>(Arg aArgs[])
-{
-    return mTcp.Process(aArgs);
-}
+template <> otError Interpreter::Process<Cmd("tcp")>(Arg aArgs[]) { return mTcp.Process(aArgs); }
 #endif
 
-template <> otError Interpreter::Process<Cmd("udp")>(Arg aArgs[])
-{
-    return mUdp.Process(aArgs);
-}
+template <> otError Interpreter::Process<Cmd("udp")>(Arg aArgs[]) { return mUdp.Process(aArgs); }
 
 template <> otError Interpreter::Process<Cmd("unsecureport")>(Arg aArgs[])
 {
@@ -4495,11 +7558,11 @@
         {
             for (uint8_t i = 0; i < numPorts; i++)
             {
-                OutputFormat("%d ", ports[i]);
+                OutputFormat("%u ", ports[i]);
             }
         }
 
-        OutputLine("");
+        OutputNewLine();
     }
     else
     {
@@ -4523,7 +7586,7 @@
     }
     else if (aArgs[0] == "ms")
     {
-        OutputLine("%lu", otInstanceGetUptime(GetInstancePtr()));
+        OutputUint64Line(otInstanceGetUptime(GetInstancePtr()));
     }
     else
     {
@@ -4535,22 +7598,36 @@
 #endif
 
 #if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD
-template <> otError Interpreter::Process<Cmd("commissioner")>(Arg aArgs[])
-{
-    return mCommissioner.Process(aArgs);
-}
+template <> otError Interpreter::Process<Cmd("commissioner")>(Arg aArgs[]) { return mCommissioner.Process(aArgs); }
 #endif
 
 #if OPENTHREAD_CONFIG_JOINER_ENABLE
-template <> otError Interpreter::Process<Cmd("joiner")>(Arg aArgs[])
-{
-    return mJoiner.Process(aArgs);
-}
+template <> otError Interpreter::Process<Cmd("joiner")>(Arg aArgs[]) { return mJoiner.Process(aArgs); }
 #endif
 
 #if OPENTHREAD_FTD
+/**
+ * @cli joinerport
+ * @code
+ * joinerport
+ * 1000
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otThreadGetJoinerUdpPort
+ */
 template <> otError Interpreter::Process<Cmd("joinerport")>(Arg aArgs[])
 {
+    /**
+     * @cli joinerport (set)
+     * @code
+     * joinerport 1000
+     * Done
+     * @endcode
+     * @cparam joinerport @ca{udp-port}
+     * @par api_copy
+     * #otThreadSetJoinerUdpPort
+     */
     return ProcessGetSet(aArgs, otThreadGetJoinerUdpPort, otThreadSetJoinerUdpPort);
 }
 #endif
@@ -4609,7 +7686,7 @@
 
         if (i == OT_EXT_ADDRESS_SIZE)
         {
-            OutputLine("Default rss : %d (lqi %d)", entry.mRssIn,
+            OutputLine("Default rss : %d (lqi %u)", entry.mRssIn,
                        otLinkConvertRssToLinkQuality(GetInstancePtr(), entry.mRssIn));
         }
         else
@@ -4708,7 +7785,7 @@
 
             if (i == OT_EXT_ADDRESS_SIZE)
             {
-                OutputLine("Default rss: %d (lqi %d)", entry.mRssIn,
+                OutputLine("Default rss: %d (lqi %u)", entry.mRssIn,
                            otLinkConvertRssToLinkQuality(GetInstancePtr(), entry.mRssIn));
             }
             else
@@ -4784,7 +7861,7 @@
                      otLinkConvertRssToLinkQuality(GetInstancePtr(), aEntry.mRssIn));
     }
 
-    OutputLine("");
+    OutputNewLine();
 }
 
 const char *Interpreter::MacFilterAddressModeToString(otMacFilterAddressMode aMode)
@@ -4866,14 +7943,7 @@
     }
     else if (ParseEnableOrDisable(aArgs[0], enable) == OT_ERROR_NONE)
     {
-        if (enable)
-        {
-            otTrelEnable(GetInstancePtr());
-        }
-        else
-        {
-            otTrelDisable(GetInstancePtr());
-        }
+        otTrelSetEnabled(GetInstancePtr(), enable);
     }
     else if (aArgs[0] == "filter")
     {
@@ -4891,7 +7961,7 @@
     {
         uint16_t           index = 0;
         otTrelPeerIterator iterator;
-        const otTrelPeer * peer;
+        const otTrelPeer  *peer;
         bool               isTable = true;
 
         if (aArgs[1] == "list")
@@ -4949,7 +8019,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;
@@ -4986,16 +8155,16 @@
 }
 
 void Interpreter::HandleDiagnosticGetResponse(otError              aError,
-                                              otMessage *          aMessage,
+                                              otMessage           *aMessage,
                                               const otMessageInfo *aMessageInfo,
-                                              void *               aContext)
+                                              void                *aContext)
 {
     static_cast<Interpreter *>(aContext)->HandleDiagnosticGetResponse(
         aError, aMessage, static_cast<const Ip6::MessageInfo *>(aMessageInfo));
 }
 
 void Interpreter::HandleDiagnosticGetResponse(otError                 aError,
-                                              const otMessage *       aMessage,
+                                              const otMessage        *aMessage,
                                               const Ip6::MessageInfo *aMessageInfo)
 {
     uint8_t               buf[16];
@@ -5015,7 +8184,7 @@
 
     while (length > 0)
     {
-        bytesToPrint = (length < sizeof(buf)) ? length : sizeof(buf);
+        bytesToPrint = Min(length, static_cast<uint16_t>(sizeof(buf)));
         otMessageRead(aMessage, otMessageGetOffset(aMessage) + bytesPrinted, buf, bytesToPrint);
 
         OutputBytes(buf, static_cast<uint8_t>(bytesToPrint));
@@ -5024,7 +8193,7 @@
         bytesPrinted += bytesToPrint;
     }
 
-    OutputLine("");
+    OutputNewLine();
 
     // Output Network Diagnostic TLV values in standard YAML format.
     while (otThreadGetNextDiagnosticTlv(aMessage, &iterator, &diagTlv) == OT_ERROR_NONE)
@@ -5043,7 +8212,7 @@
             OutputMode(kIndentSize, diagTlv.mData.mMode);
             break;
         case OT_NETWORK_DIAGNOSTIC_TLV_TIMEOUT:
-            OutputLine("Timeout: %u", diagTlv.mData.mTimeout);
+            OutputLine("Timeout: %lu", ToUlong(diagTlv.mData.mTimeout));
             break;
         case OT_NETWORK_DIAGNOSTIC_TLV_CONNECTIVITY:
             OutputLine("Connectivity:");
@@ -5093,7 +8262,21 @@
             OutputLine("'");
             break;
         case OT_NETWORK_DIAGNOSTIC_TLV_MAX_CHILD_TIMEOUT:
-            OutputLine("Max Child Timeout: %u", diagTlv.mData.mMaxChildTimeout);
+            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;
         }
     }
@@ -5145,7 +8328,7 @@
 
 void Interpreter::OutputLeaderData(uint8_t aIndentSize, const otLeaderData &aLeaderData)
 {
-    OutputLine(aIndentSize, "PartitionId: 0x%08x", aLeaderData.mPartitionId);
+    OutputLine(aIndentSize, "PartitionId: 0x%08lx", ToUlong(aLeaderData.mPartitionId));
     OutputLine(aIndentSize, "Weighting: %u", aLeaderData.mWeighting);
     OutputLine(aIndentSize, "DataVersion: %u", aLeaderData.mDataVersion);
     OutputLine(aIndentSize, "StableDataVersion: %u", aLeaderData.mStableDataVersion);
@@ -5154,15 +8337,15 @@
 
 void Interpreter::OutputNetworkDiagMacCounters(uint8_t aIndentSize, const otNetworkDiagMacCounters &aMacCounters)
 {
-    OutputLine(aIndentSize, "IfInUnknownProtos: %u", aMacCounters.mIfInUnknownProtos);
-    OutputLine(aIndentSize, "IfInErrors: %u", aMacCounters.mIfInErrors);
-    OutputLine(aIndentSize, "IfOutErrors: %u", aMacCounters.mIfOutErrors);
-    OutputLine(aIndentSize, "IfInUcastPkts: %u", aMacCounters.mIfInUcastPkts);
-    OutputLine(aIndentSize, "IfInBroadcastPkts: %u", aMacCounters.mIfInBroadcastPkts);
-    OutputLine(aIndentSize, "IfInDiscards: %u", aMacCounters.mIfInDiscards);
-    OutputLine(aIndentSize, "IfOutUcastPkts: %u", aMacCounters.mIfOutUcastPkts);
-    OutputLine(aIndentSize, "IfOutBroadcastPkts: %u", aMacCounters.mIfOutBroadcastPkts);
-    OutputLine(aIndentSize, "IfOutDiscards: %u", aMacCounters.mIfOutDiscards);
+    OutputLine(aIndentSize, "IfInUnknownProtos: %lu", ToUlong(aMacCounters.mIfInUnknownProtos));
+    OutputLine(aIndentSize, "IfInErrors: %lu", ToUlong(aMacCounters.mIfInErrors));
+    OutputLine(aIndentSize, "IfOutErrors: %lu", ToUlong(aMacCounters.mIfOutErrors));
+    OutputLine(aIndentSize, "IfInUcastPkts: %lu", ToUlong(aMacCounters.mIfInUcastPkts));
+    OutputLine(aIndentSize, "IfInBroadcastPkts: %lu", ToUlong(aMacCounters.mIfInBroadcastPkts));
+    OutputLine(aIndentSize, "IfInDiscards: %lu", ToUlong(aMacCounters.mIfInDiscards));
+    OutputLine(aIndentSize, "IfOutUcastPkts: %lu", ToUlong(aMacCounters.mIfOutUcastPkts));
+    OutputLine(aIndentSize, "IfOutBroadcastPkts: %lu", ToUlong(aMacCounters.mIfOutBroadcastPkts));
+    OutputLine(aIndentSize, "IfOutDiscards: %lu", ToUlong(aMacCounters.mIfOutDiscards));
 }
 
 void Interpreter::OutputChildTableEntry(uint8_t aIndentSize, const otNetworkDiagChildEntry &aChildEntry)
@@ -5170,10 +8353,11 @@
     OutputLine("ChildId: 0x%04x", aChildEntry.mChildId);
 
     OutputLine(aIndentSize, "Timeout: %u", aChildEntry.mTimeout);
+    OutputLine(aIndentSize, "Link Quality: %u", aChildEntry.mLinkQuality);
     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)
 {
@@ -5186,12 +8370,28 @@
     OutputResult(OT_ERROR_NONE);
 }
 
+#if OPENTHREAD_FTD
+void Interpreter::HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo *aInfo, void *aContext)
+{
+    static_cast<Interpreter *>(aContext)->HandleDiscoveryRequest(*aInfo);
+}
+
 void Interpreter::HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo &aInfo)
 {
     OutputFormat("~ Discovery Request from ");
     OutputExtAddress(aInfo.mExtAddress);
     OutputLine(": version=%u,joiner=%d", aInfo.mVersion, aInfo.mIsJoiner);
 }
+#endif
+
+#if OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK
+void Interpreter::HandleIp6Receive(otMessage *aMessage, void *aContext)
+{
+    OT_UNUSED_VARIABLE(aContext);
+
+    otMessageFree(aMessage);
+}
+#endif
 
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
 
@@ -5272,10 +8472,9 @@
         CmdEntry("child"),
         CmdEntry("childip"),
         CmdEntry("childmax"),
+        CmdEntry("childrouterlinks"),
 #endif
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
         CmdEntry("childsupervision"),
-#endif
         CmdEntry("childtimeout"),
 #if OPENTHREAD_CONFIG_COAP_API_ENABLE
         CmdEntry("coap"),
@@ -5302,6 +8501,9 @@
 #endif
         CmdEntry("detach"),
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
+#if OPENTHREAD_FTD
+        CmdEntry("deviceprops"),
+#endif
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
         CmdEntry("diag"),
 #endif
@@ -5355,6 +8557,9 @@
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
         CmdEntry("macfilter"),
 #endif
+#if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
+        CmdEntry("meshdiag"),
+#endif
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
         CmdEntry("mliid"),
 #endif
@@ -5363,12 +8568,15 @@
 #endif
         CmdEntry("mode"),
         CmdEntry("multiradio"),
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE || OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+        CmdEntry("nat64"),
+#endif
 #if OPENTHREAD_FTD
         CmdEntry("neighbor"),
 #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
@@ -5382,6 +8590,9 @@
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
         CmdEntry("networktime"),
 #endif
+#if OPENTHREAD_FTD
+        CmdEntry("nexthop"),
+#endif
         CmdEntry("panid"),
         CmdEntry("parent"),
 #if OPENTHREAD_FTD
@@ -5391,6 +8602,7 @@
 #if OPENTHREAD_CONFIG_PING_SENDER_ENABLE
         CmdEntry("ping"),
 #endif
+        CmdEntry("platform"),
         CmdEntry("pollperiod"),
 #if OPENTHREAD_FTD
         CmdEntry("preferrouterid"),
@@ -5458,6 +8670,9 @@
 #if OPENTHREAD_CONFIG_UPTIME_ENABLE
         CmdEntry("uptime"),
 #endif
+#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
+        CmdEntry("vendor"),
+#endif
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
         CmdEntry("version"),
     };
@@ -5477,9 +8692,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
@@ -5495,14 +8713,11 @@
     Interpreter::Initialize(aInstance, aCallback, aContext);
 }
 
-extern "C" void otCliInputLine(char *aBuf)
-{
-    Interpreter::GetInterpreter().ProcessLine(aBuf);
-}
+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)
@@ -5518,10 +8733,7 @@
     va_end(aAp);
 }
 
-extern "C" void otCliAppendResult(otError aError)
-{
-    Interpreter::GetInterpreter().OutputResult(aError);
-}
+extern "C" void otCliAppendResult(otError aError) { Interpreter::GetInterpreter().OutputResult(aError); }
 
 extern "C" void otCliPlatLogv(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, va_list aArgs)
 {
@@ -5534,7 +8746,7 @@
     // `EmittingCommandOutput` to false indicate this.
     Interpreter::GetInterpreter().SetEmittingCommandOutput(false);
     Interpreter::GetInterpreter().OutputFormatV(aFormat, aArgs);
-    Interpreter::GetInterpreter().OutputLine("");
+    Interpreter::GetInterpreter().OutputNewLine();
     Interpreter::GetInterpreter().SetEmittingCommandOutput(true);
 
 exit:
@@ -5543,20 +8755,3 @@
 
 } // namespace Cli
 } // namespace ot
-
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-OT_TOOL_WEAK void otNcpRegisterLegacyHandlers(const otNcpLegacyHandlers *aHandlers)
-{
-    OT_UNUSED_VARIABLE(aHandlers);
-}
-
-OT_TOOL_WEAK void otNcpHandleDidReceiveNewLegacyUlaPrefix(const uint8_t *aUlaPrefix)
-{
-    OT_UNUSED_VARIABLE(aUlaPrefix);
-}
-
-OT_TOOL_WEAK void otNcpHandleLegacyNodeDidJoin(const otExtAddress *aExtAddr)
-{
-    OT_UNUSED_VARIABLE(aExtAddr);
-}
-#endif
diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp
index f4aadbb..6c6cbbd 100644
--- a/src/cli/cli.hpp
+++ b/src/cli/cli.hpp
@@ -47,6 +47,7 @@
 #include <openthread/ip6.h>
 #include <openthread/link.h>
 #include <openthread/logging.h>
+#include <openthread/mesh_diag.h>
 #include <openthread/netdata.h>
 #include <openthread/ping_sender.h>
 #include <openthread/sntp.h>
@@ -57,6 +58,7 @@
 #include <openthread/thread_ftd.h>
 #include <openthread/udp.h>
 
+#include "cli/cli_br.hpp"
 #include "cli/cli_commissioner.hpp"
 #include "cli/cli_dataset.hpp"
 #include "cli/cli_history.hpp"
@@ -74,6 +76,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"
@@ -99,9 +102,10 @@
  * This class implements the CLI interpreter.
  *
  */
-class Interpreter : public Output
+class Interpreter : public OutputImplementer, public Output
 {
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
+    friend class Br;
     friend class Commissioner;
     friend class Joiner;
     friend class NetworkData;
@@ -177,14 +181,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.
 
@@ -235,21 +241,45 @@
      */
     static const char *PreferenceToString(signed int aPreference);
 
+    /**
+     * This method parses the argument as an IP address.
+     *
+     * If the argument string is an IPv4 address, this method will try to synthesize an IPv6 address using preferred
+     * NAT64 prefix in the network data.
+     *
+     * @param[in]  aInstance       A pointer to openthread instance.
+     * @param[in]  aArg            The argument string to parse.
+     * @param[out] aAddress        A reference to an `otIp6Address` to output the parsed IPv6 address.
+     * @param[out] aSynthesized    Whether @p aAddress is synthesized from an IPv4 address.
+     *
+     * @retval OT_ERROR_NONE          The argument was parsed successfully.
+     * @retval OT_ERROR_INVALID_ARGS  The argument is empty or does not contain valid IP address.
+     * @retval OT_ERROR_INVALID_STATE No valid NAT64 prefix in the network data.
+     *
+     */
+    static otError ParseToIp6Address(otInstance   *aInstance,
+                                     const Arg    &aArg,
+                                     otIp6Address &aAddress,
+                                     bool         &aSynthesized);
+
 protected:
     static Interpreter *sInterpreter;
 
 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;
     static constexpr uint32_t kLocateTimeoutMsecs            = 2500;
 
+    static constexpr uint16_t kMaxTxtDataSize = OPENTHREAD_CONFIG_CLI_TXT_RECORD_MAX_SIZE;
+
     using Command = CommandEntry<Interpreter>;
 
     template <typename ValueType> using GetHandler         = ValueType (&)(otInstance *);
@@ -259,13 +289,15 @@
     // Returns format string to output a `ValueType` (e.g., "%u" for `uint16_t`).
     template <typename ValueType> static constexpr const char *FormatStringFor(void);
 
+    // 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, uint32_t>::kValue || TypeTraits::IsSame<ValueType, int8_t>::kValue ||
-                TypeTraits::IsSame<ValueType, int16_t>::kValue || TypeTraits::IsSame<ValueType, int32_t>::kValue,
-            "ValueType must be an  8, 16, or 32 bit `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;
 
@@ -339,6 +371,9 @@
     static otError ParsePrefix(Arg aArgs[], otBorderRouterConfig &aConfig);
     static otError ParseRoute(Arg aArgs[], otExternalRouteConfig &aConfig);
 #endif
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+    void OutputBorderRouterCounters(void);
+#endif
 
     otError ProcessCommand(Arg aArgs[]);
 
@@ -368,14 +403,18 @@
     otError ParseLinkMetricsFlags(otLinkMetrics &aLinkMetrics, const Arg &aFlags);
 #endif
 #if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE
-    static void HandleLocateResult(void *              aContext,
+    static void HandleLocateResult(void               *aContext,
                                    otError             aError,
                                    const otIp6Address *aMeshLocalAddress,
                                    uint16_t            aRloc16);
     void        HandleLocateResult(otError aError, const otIp6Address *aMeshLocalAddress, uint16_t aRloc16);
 #endif
+#if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
+    static void HandleMeshDiagDiscoverDone(otError aError, otMeshDiagRouterInfo *aRouterInfo, void *aContext);
+    void        HandleMeshDiagDiscoverDone(otError aError, otMeshDiagRouterInfo *aRouterInfo);
+#endif
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE
-    static void HandleMlrRegResult(void *              aContext,
+    static void HandleMlrRegResult(void               *aContext,
                                    otError             aError,
                                    uint8_t             aMlrStatus,
                                    const otIp6Address *aFailedAddresses,
@@ -405,12 +444,12 @@
     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,
+                                            otMessage           *aMessage,
                                             const otMessageInfo *aMessageInfo,
-                                            void *               aContext);
+                                            void                *aContext);
 
     void OutputMode(uint8_t aIndentSize, const otLinkModeConfig &aMode);
     void OutputConnectivity(uint8_t aIndentSize, const otNetworkDiagConnectivity &aConnectivity);
@@ -425,6 +464,8 @@
     otError     GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig);
     static void HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse, void *aContext);
     void        HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse);
+    const char *DnsConfigServiceModeToString(otDnsServiceMode aMode) const;
+    otError     ParseDnsServiceMode(const Arg &aArg, otDnsServiceMode &aMode) const;
 #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);
@@ -451,12 +492,12 @@
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
     void PrintLinkMetricsValue(const otLinkMetricsValues *aMetricsValues);
 
-    static void HandleLinkMetricsReport(const otIp6Address *       aAddress,
+    static void HandleLinkMetricsReport(const otIp6Address        *aAddress,
                                         const otLinkMetricsValues *aMetricsValues,
                                         uint8_t                    aStatus,
-                                        void *                     aContext);
+                                        void                      *aContext);
 
-    void HandleLinkMetricsReport(const otIp6Address *       aAddress,
+    void HandleLinkMetricsReport(const otIp6Address        *aAddress,
                                  const otLinkMetricsValues *aMetricsValues,
                                  uint8_t                    aStatus);
 
@@ -465,12 +506,12 @@
     void HandleLinkMetricsMgmtResponse(const otIp6Address *aAddress, uint8_t aStatus);
 
     static void HandleLinkMetricsEnhAckProbingIe(otShortAddress             aShortAddress,
-                                                 const otExtAddress *       aExtAddress,
+                                                 const otExtAddress        *aExtAddress,
                                                  const otLinkMetricsValues *aMetricsValues,
-                                                 void *                     aContext);
+                                                 void                      *aContext);
 
     void HandleLinkMetricsEnhAckProbingIe(otShortAddress             aShortAddress,
-                                          const otExtAddress *       aExtAddress,
+                                          const otExtAddress        *aExtAddress,
                                           const otLinkMetricsValues *aMetricsValues);
 
     const char *LinkMetricsStatusToStr(uint8_t aStatus);
@@ -479,11 +520,14 @@
     static void HandleDetachGracefullyResult(void *aContext);
     void        HandleDetachGracefullyResult(void);
 
-    static void HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo *aInfo, void *aContext)
-    {
-        static_cast<Interpreter *>(aContext)->HandleDiscoveryRequest(*aInfo);
-    }
-    void HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo &aInfo);
+#if OPENTHREAD_FTD
+    static void HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo *aInfo, void *aContext);
+    void        HandleDiscoveryRequest(const otThreadDiscoveryRequestInfo &aInfo);
+#endif
+
+#if OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK
+    static void HandleIp6Receive(otMessage *aMessage, void *aContext);
+#endif
 
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
 
@@ -492,10 +536,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;
 
@@ -508,6 +557,10 @@
     NetworkData mNetworkData;
     UdpExample  mUdp;
 
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    Br mBr;
+#endif
+
 #if OPENTHREAD_CONFIG_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_TCP_ENABLE
     TcpExample mTcp;
 #endif
@@ -547,38 +600,50 @@
 #if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE
     bool mLocateInProgress : 1;
 #endif
+
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    bool mLinkMetricsQueryInProgress : 1;
+#endif
 };
 
 // Specializations of `FormatStringFor<ValueType>()`
 
-template <> inline constexpr const char *Interpreter::FormatStringFor<uint8_t>(void)
+template <> inline constexpr const char *Interpreter::FormatStringFor<uint8_t>(void) { return "%u"; }
+
+template <> inline constexpr const char *Interpreter::FormatStringFor<uint16_t>(void) { return "%u"; }
+
+template <> inline constexpr const char *Interpreter::FormatStringFor<uint32_t>(void) { return "%lu"; }
+
+template <> inline constexpr const char *Interpreter::FormatStringFor<int8_t>(void) { return "%d"; }
+
+template <> inline constexpr const char *Interpreter::FormatStringFor<int16_t>(void) { return "%d"; }
+
+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)
 {
-    return "%u";
+    otError error = OT_ERROR_NONE;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    OutputLine(FormatStringFor<uint32_t>(), ToUlong(aGetHandler(GetInstancePtr())));
+
+exit:
+    return error;
 }
 
-template <> inline constexpr const char *Interpreter::FormatStringFor<uint16_t>(void)
+template <> inline otError Interpreter::ProcessGet<int32_t>(Arg aArgs[], GetHandler<int32_t> aGetHandler)
 {
-    return "%u";
-}
+    otError error = OT_ERROR_NONE;
 
-template <> inline constexpr const char *Interpreter::FormatStringFor<uint32_t>(void)
-{
-    return "%u";
-}
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    OutputLine(FormatStringFor<int32_t>(), static_cast<long int>(aGetHandler(GetInstancePtr())));
 
-template <> inline constexpr const char *Interpreter::FormatStringFor<int8_t>(void)
-{
-    return "%d";
-}
-
-template <> inline constexpr const char *Interpreter::FormatStringFor<int16_t>(void)
-{
-    return "%d";
-}
-
-template <> inline constexpr const char *Interpreter::FormatStringFor<int32_t>(void)
-{
-    return "%d";
+exit:
+    return error;
 }
 
 } // namespace Cli
diff --git a/src/cli/cli_br.cpp b/src/cli/cli_br.cpp
new file mode 100644
index 0000000..a3e2152
--- /dev/null
+++ b/src/cli/cli_br.cpp
@@ -0,0 +1,537 @@
+/*
+ *  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 Border Router.
+ */
+
+#include "cli_br.hpp"
+
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+
+#include <string.h>
+
+#include <openthread/border_routing.h>
+
+#include "cli/cli.hpp"
+
+namespace ot {
+namespace Cli {
+
+/**
+ * @cli br enable
+ * @code
+ * br enable
+ * Done
+ * @endcode
+ * @par
+ * Enables the Border Routing Manager.
+ * @sa otBorderRoutingSetEnabled
+ */
+template <> otError Br::Process<Cmd("enable")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    error = otBorderRoutingSetEnabled(GetInstancePtr(), true);
+
+exit:
+    return error;
+}
+
+/**
+ * @cli br disable
+ * @code
+ * br disable
+ * Done
+ * @endcode
+ * @par
+ * Disables the Border Routing Manager.
+ * @sa otBorderRoutingSetEnabled
+ */
+template <> otError Br::Process<Cmd("disable")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    error = otBorderRoutingSetEnabled(GetInstancePtr(), false);
+
+exit:
+    return error;
+}
+
+/**
+ * @cli br state
+ * @code
+ * br state
+ * running
+ * @endcode
+ * @par api_copy
+ * #otBorderRoutingGetState
+ */
+template <> otError Br::Process<Cmd("state")>(Arg aArgs[])
+{
+    static const char *const kStateStrings[] = {
+
+        "uninitialized", // (0) OT_BORDER_ROUTING_STATE_UNINITIALIZED
+        "disabled",      // (1) OT_BORDER_ROUTING_STATE_DISABLED
+        "stopped",       // (2) OT_BORDER_ROUTING_STATE_STOPPED
+        "running",       // (3) OT_BORDER_ROUTING_STATE_RUNNING
+    };
+
+    otError error = OT_ERROR_NONE;
+
+    static_assert(0 == OT_BORDER_ROUTING_STATE_UNINITIALIZED, "STATE_UNINITIALIZED value is incorrect");
+    static_assert(1 == OT_BORDER_ROUTING_STATE_DISABLED, "STATE_DISABLED value is incorrect");
+    static_assert(2 == OT_BORDER_ROUTING_STATE_STOPPED, "STATE_STOPPED value is incorrect");
+    static_assert(3 == OT_BORDER_ROUTING_STATE_RUNNING, "STATE_RUNNING value is incorrect");
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    OutputLine("%s", Stringify(otBorderRoutingGetState(GetInstancePtr()), kStateStrings));
+
+exit:
+    return error;
+}
+
+otError Br::ParsePrefixTypeArgs(Arg aArgs[], PrefixType &aFlags)
+{
+    otError error = OT_ERROR_NONE;
+
+    aFlags = 0;
+
+    if (aArgs[0].IsEmpty())
+    {
+        aFlags = kPrefixTypeFavored | kPrefixTypeLocal;
+        ExitNow();
+    }
+
+    if (aArgs[0] == "local")
+    {
+        aFlags = kPrefixTypeLocal;
+    }
+    else if (aArgs[0] == "favored")
+    {
+        aFlags = kPrefixTypeFavored;
+    }
+    else
+    {
+        ExitNow(error = OT_ERROR_INVALID_ARGS);
+    }
+
+    VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+exit:
+    return error;
+}
+
+/**
+ * @cli br omrprefix
+ * @code
+ * br omrprefix
+ * Local: fdfc:1ff5:1512:5622::/64
+ * Favored: fdfc:1ff5:1512:5622::/64 prf:low
+ * Done
+ * @endcode
+ * @par
+ * Outputs both local and favored OMR prefix.
+ * @sa otBorderRoutingGetOmrPrefix
+ * @sa otBorderRoutingGetFavoredOmrPrefix
+ */
+template <> otError Br::Process<Cmd("omrprefix")>(Arg aArgs[])
+{
+    otError    error = OT_ERROR_NONE;
+    PrefixType outputPrefixTypes;
+
+    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputPrefixTypes));
+
+    /**
+     * @cli br omrprefix local
+     * @code
+     * br omrprefix local
+     * fdfc:1ff5:1512:5622::/64
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBorderRoutingGetOmrPrefix
+     */
+    if (outputPrefixTypes & kPrefixTypeLocal)
+    {
+        otIp6Prefix local;
+
+        SuccessOrExit(error = otBorderRoutingGetOmrPrefix(GetInstancePtr(), &local));
+
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeLocal ? "" : "Local: ");
+        OutputIp6PrefixLine(local);
+    }
+
+    /**
+     * @cli br omrprefix favored
+     * @code
+     * br omrprefix favored
+     * fdfc:1ff5:1512:5622::/64 prf:low
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBorderRoutingGetFavoredOmrPrefix
+     */
+    if (outputPrefixTypes & kPrefixTypeFavored)
+    {
+        otIp6Prefix       favored;
+        otRoutePreference preference;
+
+        SuccessOrExit(error = otBorderRoutingGetFavoredOmrPrefix(GetInstancePtr(), &favored, &preference));
+
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeFavored ? "" : "Favored: ");
+        OutputIp6Prefix(favored);
+        OutputLine(" prf:%s", Interpreter::PreferenceToString(preference));
+    }
+
+exit:
+    return error;
+}
+
+/**
+ * @cli br onlinkprefix
+ * @code
+ * br onlinkprefix
+ * Local: fd41:2650:a6f5:0::/64
+ * Favored: 2600::0:1234:da12::/64
+ * Done
+ * @endcode
+ * @par
+ * Outputs both local and favored on-link prefixes.
+ * @sa otBorderRoutingGetOnLinkPrefix
+ * @sa otBorderRoutingGetFavoredOnLinkPrefix
+ */
+template <> otError Br::Process<Cmd("onlinkprefix")>(Arg aArgs[])
+{
+    otError    error = OT_ERROR_NONE;
+    PrefixType outputPrefixTypes;
+
+    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputPrefixTypes));
+
+    /**
+     * @cli br onlinkprefix local
+     * @code
+     * br onlinkprefix local
+     * fd41:2650:a6f5:0::/64
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBorderRoutingGetOnLinkPrefix
+     */
+    if (outputPrefixTypes & kPrefixTypeLocal)
+    {
+        otIp6Prefix local;
+
+        SuccessOrExit(error = otBorderRoutingGetOnLinkPrefix(GetInstancePtr(), &local));
+
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeLocal ? "" : "Local: ");
+        OutputIp6PrefixLine(local);
+    }
+
+    /**
+     * @cli br onlinkprefix favored
+     * @code
+     * br onlinkprefix favored
+     * 2600::0:1234:da12::/64
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBorderRoutingGetFavoredOnLinkPrefix
+     */
+    if (outputPrefixTypes & kPrefixTypeFavored)
+    {
+        otIp6Prefix favored;
+
+        SuccessOrExit(error = otBorderRoutingGetFavoredOnLinkPrefix(GetInstancePtr(), &favored));
+
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeFavored ? "" : "Favored: ");
+        OutputIp6PrefixLine(favored);
+    }
+
+exit:
+    return error;
+}
+
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+
+/**
+ * @cli br nat64prefix
+ * @code
+ * br nat64prefix
+ * Local: fd14:1078:b3d5:b0b0:0:0::/96
+ * Favored: fd14:1078:b3d5:b0b0:0:0::/96 prf:low
+ * Done
+ * @endcode
+ * @par
+ * Outputs both local and favored NAT64 prefixes.
+ * @sa otBorderRoutingGetNat64Prefix
+ * @sa otBorderRoutingGetFavoredNat64Prefix
+ */
+template <> otError Br::Process<Cmd("nat64prefix")>(Arg aArgs[])
+{
+    otError    error = OT_ERROR_NONE;
+    PrefixType outputPrefixTypes;
+
+    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputPrefixTypes));
+
+    /**
+     * @cli br nat64prefix local
+     * @code
+     * br nat64prefix local
+     * fd14:1078:b3d5:b0b0:0:0::/96
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBorderRoutingGetNat64Prefix
+     */
+    if (outputPrefixTypes & kPrefixTypeLocal)
+    {
+        otIp6Prefix local;
+
+        SuccessOrExit(error = otBorderRoutingGetNat64Prefix(GetInstancePtr(), &local));
+
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeLocal ? "" : "Local: ");
+        OutputIp6PrefixLine(local);
+    }
+
+    /**
+     * @cli br nat64prefix favored
+     * @code
+     * br nat64prefix favored
+     * fd14:1078:b3d5:b0b0:0:0::/96 prf:low
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBorderRoutingGetFavoredNat64Prefix
+     */
+    if (outputPrefixTypes & kPrefixTypeFavored)
+    {
+        otIp6Prefix       favored;
+        otRoutePreference preference;
+
+        SuccessOrExit(error = otBorderRoutingGetFavoredNat64Prefix(GetInstancePtr(), &favored, &preference));
+
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeFavored ? "" : "Favored: ");
+        OutputIp6Prefix(favored);
+        OutputLine(" prf:%s", Interpreter::PreferenceToString(preference));
+    }
+
+exit:
+    return error;
+}
+
+#endif // OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+
+/**
+ * @cli br prefixtable
+ * @code
+ * br prefixtable
+ * prefix:fd00:1234:5678:0::/64, on-link:no, ms-since-rx:29526, lifetime:1800, route-prf:med,
+ * router:ff02:0:0:0:0:0:0:1
+ * prefix:1200:abba:baba:0::/64, on-link:yes, ms-since-rx:29527, lifetime:1800, preferred:1800,
+ * router:ff02:0:0:0:0:0:0:1
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otBorderRoutingGetNextPrefixTableEntry
+ */
+template <> otError Br::Process<Cmd("prefixtable")>(Arg aArgs[])
+{
+    otError                            error = OT_ERROR_NONE;
+    otBorderRoutingPrefixTableIterator iterator;
+    otBorderRoutingPrefixTableEntry    entry;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    otBorderRoutingPrefixTableInitIterator(GetInstancePtr(), &iterator);
+
+    while (otBorderRoutingGetNextPrefixTableEntry(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
+    {
+        char string[OT_IP6_PREFIX_STRING_SIZE];
+
+        otIp6PrefixToString(&entry.mPrefix, string, sizeof(string));
+        OutputFormat("prefix:%s, on-link:%s, ms-since-rx:%lu, lifetime:%lu, ", string, entry.mIsOnLink ? "yes" : "no",
+                     ToUlong(entry.mMsecSinceLastUpdate), ToUlong(entry.mValidLifetime));
+
+        if (entry.mIsOnLink)
+        {
+            OutputFormat("preferred:%lu, ", ToUlong(entry.mPreferredLifetime));
+        }
+        else
+        {
+            OutputFormat("route-prf:%s, ", Interpreter::PreferenceToString(entry.mRoutePreference));
+        }
+
+        otIp6AddressToString(&entry.mRouterAddress, string, sizeof(string));
+        OutputLine("router:%s", string);
+    }
+
+exit:
+    return error;
+}
+
+template <> otError Br::Process<Cmd("rioprf")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    /**
+     * @cli br rioprf
+     * @code
+     * br rioprf
+     * med
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBorderRoutingGetRouteInfoOptionPreference
+     */
+    if (aArgs[0].IsEmpty())
+    {
+        OutputLine("%s",
+                   Interpreter::PreferenceToString(otBorderRoutingGetRouteInfoOptionPreference(GetInstancePtr())));
+    }
+    /**
+     * @cli br rioprf clear
+     * @code
+     * br rioprf clear
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otBorderRoutingClearRouteInfoOptionPreference
+     */
+    else if (aArgs[0] == "clear")
+    {
+        otBorderRoutingClearRouteInfoOptionPreference(GetInstancePtr());
+    }
+    /**
+     * @cli br rioprf (high,med,low)
+     * @code
+     * br rioprf low
+     * Done
+     * @endcode
+     * @cparam br rioprf [@ca{high}|@ca{med}|@ca{low}]
+     * @par api_copy
+     * #otBorderRoutingSetRouteInfoOptionPreference
+     */
+    else
+    {
+        otRoutePreference preference;
+
+        SuccessOrExit(error = Interpreter::ParsePreference(aArgs[0], preference));
+        otBorderRoutingSetRouteInfoOptionPreference(GetInstancePtr(), preference);
+    }
+
+exit:
+    return error;
+}
+
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+
+/**
+ * @cli br counters
+ * @code
+ * br counters
+ * Inbound Unicast: Packets 4 Bytes 320
+ * Inbound Multicast: Packets 0 Bytes 0
+ * Outbound Unicast: Packets 2 Bytes 160
+ * Outbound Multicast: Packets 0 Bytes 0
+ * RA Rx: 4
+ * RA TxSuccess: 2
+ * RA TxFailed: 0
+ * RS Rx: 0
+ * RS TxSuccess: 2
+ * RS TxFailed: 0
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otIp6GetBorderRoutingCounters
+ */
+template <> otError Br::Process<Cmd("counters")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    Interpreter::GetInterpreter().OutputBorderRouterCounters();
+
+exit:
+    return error;
+}
+
+#endif // OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+
+otError Br::Process(Arg aArgs[])
+{
+#define CmdEntry(aCommandString)                          \
+    {                                                     \
+        aCommandString, &Br::Process<Cmd(aCommandString)> \
+    }
+
+    static constexpr Command kCommands[] = {
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+        CmdEntry("counters"),
+#endif
+        CmdEntry("disable"),
+        CmdEntry("enable"),
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+        CmdEntry("nat64prefix"),
+#endif
+        CmdEntry("omrprefix"),
+        CmdEntry("onlinkprefix"),
+        CmdEntry("prefixtable"),
+        CmdEntry("rioprf"),
+        CmdEntry("state"),
+    };
+
+#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_CONFIG_BORDER_ROUTING_ENABLE
diff --git a/src/cli/cli_br.hpp b/src/cli/cli_br.hpp
new file mode 100644
index 0000000..794f874
--- /dev/null
+++ b/src/cli/cli_br.hpp
@@ -0,0 +1,96 @@
+/*
+ *  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 Border Router.
+ */
+
+#ifndef CLI_BR_HPP_
+#define CLI_BR_HPP_
+
+#include "openthread-core-config.h"
+
+#include "cli/cli_config.h"
+#include "cli/cli_output.hpp"
+
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+
+namespace ot {
+namespace Cli {
+
+/**
+ * This class implements the Border Router CLI interpreter.
+ *
+ */
+class Br : private Output
+{
+public:
+    typedef Utils::CmdLineParser::Arg Arg;
+
+    /**
+     * Constructor
+     *
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
+     *
+     */
+    Br(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:
+    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[], PrefixType &aFlags);
+};
+
+} // namespace Cli
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+
+#endif // CLI_BR_HPP_
diff --git a/src/cli/cli_coap.cpp b/src/cli/cli_coap.cpp
index 5d7c260..7f71781 100644
--- a/src/cli/cli_coap.cpp
+++ b/src/cli/cli_coap.cpp
@@ -44,8 +44,8 @@
 namespace ot {
 namespace Cli {
 
-Coap::Coap(Output &aOutput)
-    : OutputWrapper(aOutput)
+Coap::Coap(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+    : Output(aInstance, aOutputImplementer)
     , mUseDefaultRequestTxParameters(true)
     , mUseDefaultResponseTxParameters(true)
 #if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
@@ -75,7 +75,7 @@
 otError Coap::CancelResourceSubscription(void)
 {
     otError       error   = OT_ERROR_NONE;
-    otMessage *   message = nullptr;
+    otMessage    *message = nullptr;
     otMessageInfo messageInfo;
 
     memset(&messageInfo, 0, sizeof(messageInfo));
@@ -128,7 +128,7 @@
 
         while (length > 0)
         {
-            bytesToPrint = (length < sizeof(buf)) ? length : sizeof(buf);
+            bytesToPrint = Min(length, static_cast<uint16_t>(sizeof(buf)));
             otMessageRead(aMessage, otMessageGetOffset(aMessage) + bytesPrinted, buf, bytesToPrint);
 
             OutputBytes(buf, static_cast<uint8_t>(bytesToPrint));
@@ -138,7 +138,7 @@
         }
     }
 
-    OutputLine("");
+    OutputNewLine();
 }
 
 #if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
@@ -192,7 +192,7 @@
 template <> otError Coap::Process<Cmd("set")>(Arg aArgs[])
 {
 #if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
-    otMessage *   notificationMessage = nullptr;
+    otMessage    *notificationMessage = nullptr;
     otMessageInfo messageInfo;
 #endif
     otError error = OT_ERROR_NONE;
@@ -273,7 +273,7 @@
 template <> otError Coap::Process<Cmd("parameters")>(Arg aArgs[])
 {
     otError             error = OT_ERROR_NONE;
-    bool *              defaultTxParameters;
+    bool               *defaultTxParameters;
     otCoapTxParameters *txParameters;
 
     if (aArgs[0] == "request")
@@ -319,7 +319,7 @@
     }
     else
     {
-        OutputLine("ACK_TIMEOUT=%u ms, ACK_RANDOM_FACTOR=%u/%u, MAX_RETRANSMIT=%u", txParameters->mAckTimeout,
+        OutputLine("ACK_TIMEOUT=%lu ms, ACK_RANDOM_FACTOR=%u/%u, MAX_RETRANSMIT=%u", ToUlong(txParameters->mAckTimeout),
                    txParameters->mAckRandomFactorNumerator, txParameters->mAckRandomFactorDenominator,
                    txParameters->mMaxRetransmit);
     }
@@ -328,25 +328,13 @@
     return error;
 }
 
-template <> otError Coap::Process<Cmd("get")>(Arg aArgs[])
-{
-    return ProcessRequest(aArgs, OT_COAP_CODE_GET);
-}
+template <> otError Coap::Process<Cmd("get")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_GET); }
 
-template <> otError Coap::Process<Cmd("post")>(Arg aArgs[])
-{
-    return ProcessRequest(aArgs, OT_COAP_CODE_POST);
-}
+template <> otError Coap::Process<Cmd("post")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_POST); }
 
-template <> otError Coap::Process<Cmd("put")>(Arg aArgs[])
-{
-    return ProcessRequest(aArgs, OT_COAP_CODE_PUT);
-}
+template <> otError Coap::Process<Cmd("put")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_PUT); }
 
-template <> otError Coap::Process<Cmd("delete")>(Arg aArgs[])
-{
-    return ProcessRequest(aArgs, OT_COAP_CODE_DELETE);
-}
+template <> otError Coap::Process<Cmd("delete")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_DELETE); }
 
 #if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
 template <> otError Coap::Process<Cmd("observe")>(Arg aArgs[])
@@ -362,7 +350,7 @@
 #endif
 {
     otError       error   = OT_ERROR_NONE;
-    otMessage *   message = nullptr;
+    otMessage    *message = nullptr;
     otMessageInfo messageInfo;
     uint16_t      payloadLength = 0;
 
@@ -647,7 +635,8 @@
             SuccessOrExit(error = otCoapOptionIteratorGetOptionUintValue(&iterator, &observe));
             observePresent = true;
 
-            OutputFormat(" OBS=%lu", static_cast<uint32_t>(observe));
+            OutputFormat(" OBS=");
+            OutputUint64(observe);
         }
 #endif
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
@@ -799,8 +788,8 @@
 }
 
 #if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
-void Coap::HandleNotificationResponse(void *               aContext,
-                                      otMessage *          aMessage,
+void Coap::HandleNotificationResponse(void                *aContext,
+                                      otMessage           *aMessage,
                                       const otMessageInfo *aMessageInfo,
                                       otError              aError)
 {
@@ -862,7 +851,8 @@
 
                 if (error == OT_ERROR_NONE)
                 {
-                    OutputFormat(" OBS=%u", observeVal);
+                    OutputFormat(" OBS=");
+                    OutputUint64(observeVal);
                 }
             }
         }
@@ -872,7 +862,7 @@
 }
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-otError Coap::BlockwiseReceiveHook(void *         aContext,
+otError Coap::BlockwiseReceiveHook(void          *aContext,
                                    const uint8_t *aBlock,
                                    uint32_t       aPosition,
                                    uint16_t       aBlockLength,
@@ -901,11 +891,11 @@
     return OT_ERROR_NONE;
 }
 
-otError Coap::BlockwiseTransmitHook(void *    aContext,
-                                    uint8_t * aBlock,
+otError Coap::BlockwiseTransmitHook(void     *aContext,
+                                    uint8_t  *aBlock,
                                     uint32_t  aPosition,
                                     uint16_t *aBlockLength,
-                                    bool *    aMore)
+                                    bool     *aMore)
 {
     return static_cast<Coap *>(aContext)->BlockwiseTransmitHook(aBlock, aPosition, aBlockLength, aMore);
 }
diff --git a/src/cli/cli_coap.hpp b/src/cli/cli_coap.hpp
index afa56bf..041e7ae 100644
--- a/src/cli/cli_coap.hpp
+++ b/src/cli/cli_coap.hpp
@@ -49,7 +49,7 @@
  * This class implements the CLI CoAP server and client.
  *
  */
-class Coap : private OutputWrapper
+class Coap : private Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg;
@@ -57,10 +57,11 @@
     /**
      * Constructor
      *
-     * @param[in]  aOutput The CLI console output context
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
      *
      */
-    explicit Coap(Output &aOutput);
+    Coap(otInstance *aInstance, OutputImplementer &aOutputImplementer);
 
     /**
      * This method interprets a list of CLI arguments.
@@ -105,8 +106,8 @@
     void        HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo);
 
 #if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
-    static void HandleNotificationResponse(void *               aContext,
-                                           otMessage *          aMessage,
+    static void HandleNotificationResponse(void                *aContext,
+                                           otMessage           *aMessage,
                                            const otMessageInfo *aMessageInfo,
                                            otError              aError);
     void        HandleNotificationResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError);
@@ -117,7 +118,7 @@
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
 
-    static otError BlockwiseReceiveHook(void *         aContext,
+    static otError BlockwiseReceiveHook(void          *aContext,
                                         const uint8_t *aBlock,
                                         uint32_t       aPosition,
                                         uint16_t       aBlockLength,
@@ -128,11 +129,11 @@
                                         uint16_t       aBlockLength,
                                         bool           aMore,
                                         uint32_t       aTotalLength);
-    static otError BlockwiseTransmitHook(void *    aContext,
-                                         uint8_t * aBlock,
+    static otError BlockwiseTransmitHook(void     *aContext,
+                                         uint8_t  *aBlock,
                                          uint32_t  aPosition,
                                          uint16_t *aBlockLength,
-                                         bool *    aMore);
+                                         bool     *aMore);
     otError        BlockwiseTransmitHook(uint8_t *aBlock, uint32_t aPosition, uint16_t *aBlockLength, bool *aMore);
 #endif
 
diff --git a/src/cli/cli_coap_secure.cpp b/src/cli/cli_coap_secure.cpp
index aae4798..518e907 100644
--- a/src/cli/cli_coap_secure.cpp
+++ b/src/cli/cli_coap_secure.cpp
@@ -46,10 +46,8 @@
 namespace ot {
 namespace Cli {
 
-constexpr CoapSecure::Command CoapSecure::sCommands[];
-
-CoapSecure::CoapSecure(Output &aOutput)
-    : OutputWrapper(aOutput)
+CoapSecure::CoapSecure(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+    : Output(aInstance, aOutputImplementer)
     , mShutdownFlag(false)
     , mUseCertificate(false)
     , mPskLength(0)
@@ -79,7 +77,7 @@
 
         while (length > 0)
         {
-            bytesToPrint = (length < sizeof(buf)) ? length : sizeof(buf);
+            bytesToPrint = Min(length, static_cast<uint16_t>(sizeof(buf)));
             otMessageRead(aMessage, otMessageGetOffset(aMessage) + bytesPrinted, buf, bytesToPrint);
 
             OutputBytes(buf, static_cast<uint8_t>(bytesToPrint));
@@ -89,22 +87,10 @@
         }
     }
 
-    OutputLine("");
+    OutputNewLine();
 }
 
-otError CoapSecure::ProcessHelp(Arg aArgs[])
-{
-    OT_UNUSED_VARIABLE(aArgs);
-
-    for (const Command &command : sCommands)
-    {
-        OutputLine(command.mName);
-    }
-
-    return OT_ERROR_NONE;
-}
-
-otError CoapSecure::ProcessResource(Arg aArgs[])
+template <> otError CoapSecure::Process<Cmd("resource")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
 
@@ -142,7 +128,7 @@
     return error;
 }
 
-otError CoapSecure::ProcessSet(Arg aArgs[])
+template <> otError CoapSecure::Process<Cmd("set")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
 
@@ -161,7 +147,7 @@
     return error;
 }
 
-otError CoapSecure::ProcessStart(Arg aArgs[])
+template <> otError CoapSecure::Process<Cmd("start")>(Arg aArgs[])
 {
     otError error          = OT_ERROR_NONE;
     bool    verifyPeerCert = true;
@@ -191,7 +177,7 @@
     return error;
 }
 
-otError CoapSecure::ProcessStop(Arg aArgs[])
+template <> otError CoapSecure::Process<Cmd("stop")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
 
@@ -214,22 +200,13 @@
     return OT_ERROR_NONE;
 }
 
-otError CoapSecure::ProcessGet(Arg aArgs[])
-{
-    return ProcessRequest(aArgs, OT_COAP_CODE_GET);
-}
+template <> otError CoapSecure::Process<Cmd("get")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_GET); }
 
-otError CoapSecure::ProcessPost(Arg aArgs[])
-{
-    return ProcessRequest(aArgs, OT_COAP_CODE_POST);
-}
+template <> otError CoapSecure::Process<Cmd("post")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_POST); }
 
-otError CoapSecure::ProcessPut(Arg aArgs[])
-{
-    return ProcessRequest(aArgs, OT_COAP_CODE_PUT);
-}
+template <> otError CoapSecure::Process<Cmd("put")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_PUT); }
 
-otError CoapSecure::ProcessDelete(Arg aArgs[])
+template <> otError CoapSecure::Process<Cmd("delete")>(Arg aArgs[])
 {
     return ProcessRequest(aArgs, OT_COAP_CODE_DELETE);
 }
@@ -384,7 +361,7 @@
     return error;
 }
 
-otError CoapSecure::ProcessConnect(Arg aArgs[])
+template <> otError CoapSecure::Process<Cmd("connect")>(Arg aArgs[])
 {
     otError    error;
     otSockAddr sockaddr;
@@ -404,7 +381,7 @@
     return error;
 }
 
-otError CoapSecure::ProcessDisconnect(Arg aArgs[])
+template <> otError CoapSecure::Process<Cmd("disconnect")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
 
@@ -414,7 +391,7 @@
 }
 
 #ifdef MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
-otError CoapSecure::ProcessPsk(Arg aArgs[])
+template <> otError CoapSecure::Process<Cmd("psk")>(Arg aArgs[])
 {
     otError  error = OT_ERROR_NONE;
     uint16_t length;
@@ -440,7 +417,7 @@
 #endif // MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
 
 #ifdef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
-otError CoapSecure::ProcessX509(Arg aArgs[])
+template <> otError CoapSecure::Process<Cmd("x509")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
 
@@ -459,17 +436,37 @@
 
 otError CoapSecure::Process(Arg aArgs[])
 {
-    otError        error = OT_ERROR_INVALID_ARGS;
-    const Command *command;
-
-    if (aArgs[0].IsEmpty())
-    {
-        IgnoreError(ProcessHelp(aArgs));
-        ExitNow();
+#define CmdEntry(aCommandString)                                  \
+    {                                                             \
+        aCommandString, &CoapSecure::Process<Cmd(aCommandString)> \
     }
 
-    command = BinarySearch::Find(aArgs[0].GetCString(), sCommands);
-    VerifyOrExit(command != nullptr, error = OT_ERROR_INVALID_COMMAND);
+    static constexpr Command kCommands[] = {
+        CmdEntry("connect"), CmdEntry("delete"),   CmdEntry("disconnect"), CmdEntry("get"),   CmdEntry("post"),
+#ifdef MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
+        CmdEntry("psk"),
+#endif
+        CmdEntry("put"),     CmdEntry("resource"), CmdEntry("set"),        CmdEntry("start"), CmdEntry("stop"),
+#ifdef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
+        CmdEntry("x509"),
+#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() ? OT_ERROR_INVALID_ARGS : OT_ERROR_NONE);
+    }
+
+    command = BinarySearch::Find(aArgs[0].GetCString(), kCommands);
+    VerifyOrExit(command != nullptr);
 
     error = (this->*command->mHandler)(aArgs + 1);
 
@@ -688,7 +685,7 @@
 #endif // CLI_COAP_SECURE_USE_COAP_DEFAULT_HANDLER
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-otError CoapSecure::BlockwiseReceiveHook(void *         aContext,
+otError CoapSecure::BlockwiseReceiveHook(void          *aContext,
                                          const uint8_t *aBlock,
                                          uint32_t       aPosition,
                                          uint16_t       aBlockLength,
@@ -718,11 +715,11 @@
     return OT_ERROR_NONE;
 }
 
-otError CoapSecure::BlockwiseTransmitHook(void *    aContext,
-                                          uint8_t * aBlock,
+otError CoapSecure::BlockwiseTransmitHook(void     *aContext,
+                                          uint8_t  *aBlock,
                                           uint32_t  aPosition,
                                           uint16_t *aBlockLength,
-                                          bool *    aMore)
+                                          bool     *aMore)
 {
     return static_cast<CoapSecure *>(aContext)->BlockwiseTransmitHook(aBlock, aPosition, aBlockLength, aMore);
 }
diff --git a/src/cli/cli_coap_secure.hpp b/src/cli/cli_coap_secure.hpp
index 3bd4872..9bdc631 100644
--- a/src/cli/cli_coap_secure.hpp
+++ b/src/cli/cli_coap_secure.hpp
@@ -55,7 +55,7 @@
  * This class implements the CLI CoAP Secure server and client.
  *
  */
-class CoapSecure : private OutputWrapper
+class CoapSecure : private Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg;
@@ -63,10 +63,11 @@
     /**
      * Constructor
      *
-     * @param[in]  aOutput The CLI console output context
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
      *
      */
-    explicit CoapSecure(Output &aOutput);
+    CoapSecure(otInstance *aInstance, OutputImplementer &aOutputImplementer);
 
     /**
      * This method interprets a list of CLI arguments.
@@ -96,19 +97,7 @@
 
     void PrintPayload(otMessage *aMessage);
 
-    otError ProcessConnect(Arg aArgs[]);
-    otError ProcessDelete(Arg aArgs[]);
-    otError ProcessDisconnect(Arg aArgs[]);
-    otError ProcessGet(Arg aArgs[]);
-    otError ProcessHelp(Arg aArgs[]);
-    otError ProcessPost(Arg aArgs[]);
-    otError ProcessPsk(Arg aArgs[]);
-    otError ProcessPut(Arg aArgs[]);
-    otError ProcessResource(Arg aArgs[]);
-    otError ProcessSet(Arg aArgs[]);
-    otError ProcessStart(Arg aArgs[]);
-    otError ProcessStop(Arg aArgs[]);
-    otError ProcessX509(Arg aArgs[]);
+    template <CommandId kCommandId> otError Process(Arg aArgs[]);
 
     otError ProcessRequest(Arg aArgs[], otCoapCode aCoapCode);
 
@@ -122,7 +111,7 @@
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
 
-    static otError BlockwiseReceiveHook(void *         aContext,
+    static otError BlockwiseReceiveHook(void          *aContext,
                                         const uint8_t *aBlock,
                                         uint32_t       aPosition,
                                         uint16_t       aBlockLength,
@@ -133,11 +122,11 @@
                                         uint16_t       aBlockLength,
                                         bool           aMore,
                                         uint32_t       aTotalLength);
-    static otError BlockwiseTransmitHook(void *    aContext,
-                                         uint8_t * aBlock,
+    static otError BlockwiseTransmitHook(void     *aContext,
+                                         uint8_t  *aBlock,
                                          uint32_t  aPosition,
                                          uint16_t *aBlockLength,
-                                         bool *    aMore);
+                                         bool     *aMore);
     otError        BlockwiseTransmitHook(uint8_t *aBlock, uint32_t aPosition, uint16_t *aBlockLength, bool *aMore);
 #endif
 
@@ -149,28 +138,6 @@
     static void HandleConnected(bool aConnected, void *aContext);
     void        HandleConnected(bool aConnected);
 
-    static constexpr Command sCommands[] = {
-        {"connect", &CoapSecure::ProcessConnect},
-        {"delete", &CoapSecure::ProcessDelete},
-        {"disconnect", &CoapSecure::ProcessDisconnect},
-        {"get", &CoapSecure::ProcessGet},
-        {"help", &CoapSecure::ProcessHelp},
-        {"post", &CoapSecure::ProcessPost},
-#ifdef MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
-        {"psk", &CoapSecure::ProcessPsk},
-#endif
-        {"put", &CoapSecure::ProcessPut},
-        {"resource", &CoapSecure::ProcessResource},
-        {"set", &CoapSecure::ProcessSet},
-        {"start", &CoapSecure::ProcessStart},
-        {"stop", &CoapSecure::ProcessStop},
-#ifdef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
-        {"x509", &CoapSecure::ProcessX509},
-#endif
-    };
-
-    static_assert(BinarySearch::IsSorted(sCommands), "Command Table is not sorted");
-
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
     otCoapBlockwiseResource mResource;
 #else
diff --git a/src/cli/cli_commissioner.cpp b/src/cli/cli_commissioner.cpp
index bbbcfce..d8fa14a 100644
--- a/src/cli/cli_commissioner.cpp
+++ b/src/cli/cli_commissioner.cpp
@@ -117,13 +117,15 @@
                 break;
 
             case OT_JOINER_INFO_TYPE_DISCERNER:
-                OutputFormat("| 0x%016llx/%2u", static_cast<unsigned long long>(joinerInfo.mSharedId.mDiscerner.mValue),
+                OutputFormat("| 0x%08lx%08lx/%2u",
+                             static_cast<unsigned long>(joinerInfo.mSharedId.mDiscerner.mValue >> 32),
+                             static_cast<unsigned long>(joinerInfo.mSharedId.mDiscerner.mValue & 0xffffffff),
                              joinerInfo.mSharedId.mDiscerner.mLength);
                 break;
             }
 
-            OutputFormat(" | %32s | %10d |", joinerInfo.mPskd.m8, joinerInfo.mExpirationTime);
-            OutputLine("");
+            OutputFormat(" | %32s | %10lu |", joinerInfo.mPskd.m8, ToUlong(joinerInfo.mExpirationTime));
+            OutputNewLine();
         }
 
         ExitNow(error = OT_ERROR_NONE);
@@ -383,16 +385,16 @@
 }
 
 void Commissioner::HandleJoinerEvent(otCommissionerJoinerEvent aEvent,
-                                     const otJoinerInfo *      aJoinerInfo,
-                                     const otExtAddress *      aJoinerId,
-                                     void *                    aContext)
+                                     const otJoinerInfo       *aJoinerInfo,
+                                     const otExtAddress       *aJoinerId,
+                                     void                     *aContext)
 {
     static_cast<Commissioner *>(aContext)->HandleJoinerEvent(aEvent, aJoinerInfo, aJoinerId);
 }
 
 void Commissioner::HandleJoinerEvent(otCommissionerJoinerEvent aEvent,
-                                     const otJoinerInfo *      aJoinerInfo,
-                                     const otExtAddress *      aJoinerId)
+                                     const otJoinerInfo       *aJoinerInfo,
+                                     const otExtAddress       *aJoinerId)
 {
     static const char *const kEventStrings[] = {
         "start",    // (0) OT_COMMISSIONER_JOINER_START
@@ -417,7 +419,7 @@
         OutputExtAddress(*aJoinerId);
     }
 
-    OutputLine("");
+    OutputNewLine();
 }
 
 template <> otError Commissioner::Process<Cmd("stop")>(Arg aArgs[])
@@ -431,7 +433,7 @@
 {
     OT_UNUSED_VARIABLE(aArgs);
 
-    OutputLine(StateToString(otCommissionerGetState(GetInstancePtr())));
+    OutputLine("%s", StateToString(otCommissionerGetState(GetInstancePtr())));
 
     return OT_ERROR_NONE;
 }
@@ -474,21 +476,21 @@
 void Commissioner::HandleEnergyReport(uint32_t       aChannelMask,
                                       const uint8_t *aEnergyList,
                                       uint8_t        aEnergyListLength,
-                                      void *         aContext)
+                                      void          *aContext)
 {
     static_cast<Commissioner *>(aContext)->HandleEnergyReport(aChannelMask, aEnergyList, aEnergyListLength);
 }
 
 void Commissioner::HandleEnergyReport(uint32_t aChannelMask, const uint8_t *aEnergyList, uint8_t aEnergyListLength)
 {
-    OutputFormat("Energy: %08x ", aChannelMask);
+    OutputFormat("Energy: %08lx ", ToUlong(aChannelMask));
 
     for (uint8_t i = 0; i < aEnergyListLength; i++)
     {
         OutputFormat("%d ", static_cast<int8_t>(aEnergyList[i]));
     }
 
-    OutputLine("");
+    OutputNewLine();
 }
 
 void Commissioner::HandlePanIdConflict(uint16_t aPanId, uint32_t aChannelMask, void *aContext)
@@ -498,7 +500,7 @@
 
 void Commissioner::HandlePanIdConflict(uint16_t aPanId, uint32_t aChannelMask)
 {
-    OutputLine("Conflict: %04x, %08x", aPanId, aChannelMask);
+    OutputLine("Conflict: %04x, %08lx", aPanId, ToUlong(aChannelMask));
 }
 
 } // namespace Cli
diff --git a/src/cli/cli_commissioner.hpp b/src/cli/cli_commissioner.hpp
index d63089d..7b63cb0 100644
--- a/src/cli/cli_commissioner.hpp
+++ b/src/cli/cli_commissioner.hpp
@@ -49,7 +49,7 @@
  * This class implements the Commissioner CLI interpreter.
  *
  */
-class Commissioner : private OutputWrapper
+class Commissioner : private Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg;
@@ -57,11 +57,12 @@
     /**
      * Constructor
      *
-     * @param[in]  aOutput The CLI console output context
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
      *
      */
-    explicit Commissioner(Output &aOutput)
-        : OutputWrapper(aOutput)
+    Commissioner(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+        : Output(aInstance, aOutputImplementer)
     {
     }
 
@@ -87,17 +88,17 @@
     void        HandleStateChanged(otCommissionerState aState);
 
     static void HandleJoinerEvent(otCommissionerJoinerEvent aEvent,
-                                  const otJoinerInfo *      aJoinerInfo,
-                                  const otExtAddress *      aJoinerId,
-                                  void *                    aContext);
+                                  const otJoinerInfo       *aJoinerInfo,
+                                  const otExtAddress       *aJoinerId,
+                                  void                     *aContext);
     void        HandleJoinerEvent(otCommissionerJoinerEvent aEvent,
-                                  const otJoinerInfo *      aJoinerInfo,
-                                  const otExtAddress *      aJoinerId);
+                                  const otJoinerInfo       *aJoinerInfo,
+                                  const otExtAddress       *aJoinerId);
 
     static void HandleEnergyReport(uint32_t       aChannelMask,
                                    const uint8_t *aEnergyList,
                                    uint8_t        aEnergyListLength,
-                                   void *         aContext);
+                                   void          *aContext);
     void        HandleEnergyReport(uint32_t aChannelMask, const uint8_t *aEnergyList, uint8_t aEnergyListLength);
 
     static void HandlePanIdConflict(uint16_t aPanId, uint32_t aChannelMask, void *aContext);
diff --git a/src/cli/cli_config.h b/src/cli/cli_config.h
index 36aedc6..4e03052 100644
--- a/src/cli/cli_config.h
+++ b/src/cli/cli_config.h
@@ -88,6 +88,16 @@
 #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_LOG_INPUT_OUTPUT_ENABLE
  *
  * Define as 1 for CLI to emit its command input string and the resulting output to the logs.
@@ -102,6 +112,18 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL
+ *
+ * Defines the log level to use when CLI emits its command input/output to the logs.
+ *
+ * This is used only when `OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE` is enabled.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL
+#define OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL OT_LOG_LEVEL_DEBG
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LOG_STRING_SIZE
  *
  * The log string buffer size (in bytes).
@@ -126,4 +148,30 @@
 #define OPENTHREAD_CONFIG_CLI_PROMPT_ENABLE 1
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_CLI_TXT_RECORD_MAX_SIZE
+ *
+ * Specifies the max TXT record data length to use when performing DNS queries.
+ *
+ * If the service TXT record data length is greater than the specified value, it will be read partially (up to the given
+ * size) and output as a sequence of raw hex bytes `[{hex-bytes}...]`
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_CLI_TXT_RECORD_MAX_SIZE
+#define OPENTHREAD_CONFIG_CLI_TXT_RECORD_MAX_SIZE 512
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK
+ *
+ * Define as 1 to have CLI register an IPv6 receive callback using `otIp6SetReceiveCallback()`.
+ *
+ * This is intended for testing only. Receive callback should be registered for the `otIp6GetBorderRoutingCounters()`
+ * to count the messages being passed to the callback.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK
+#define OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK 0
+#endif
+
 #endif // CONFIG_CLI_H_
diff --git a/src/cli/cli_dataset.cpp b/src/cli/cli_dataset.cpp
index 573436a..633fa5b 100644
--- a/src/cli/cli_dataset.cpp
+++ b/src/cli/cli_dataset.cpp
@@ -45,130 +45,182 @@
 namespace ot {
 namespace Cli {
 
-otOperationalDataset Dataset::sDataset;
+otOperationalDatasetTlvs Dataset::sDatasetTlvs;
 
-otError Dataset::Print(otOperationalDataset &aDataset)
+otError Dataset::Print(otOperationalDatasetTlvs &aDatasetTlvs)
 {
-    if (aDataset.mComponents.mIsPendingTimestampPresent)
+    otError              error;
+    otOperationalDataset dataset;
+
+    SuccessOrExit(error = otDatasetParseTlvs(&aDatasetTlvs, &dataset));
+
+    if (dataset.mComponents.mIsPendingTimestampPresent)
     {
-        OutputLine("Pending Timestamp: %lu", aDataset.mPendingTimestamp.mSeconds);
+        OutputFormat("Pending Timestamp: ");
+        OutputUint64Line(dataset.mPendingTimestamp.mSeconds);
     }
 
-    if (aDataset.mComponents.mIsActiveTimestampPresent)
+    if (dataset.mComponents.mIsActiveTimestampPresent)
     {
-        OutputLine("Active Timestamp: %lu", aDataset.mActiveTimestamp.mSeconds);
+        OutputFormat("Active Timestamp: ");
+        OutputUint64Line(dataset.mActiveTimestamp.mSeconds);
     }
 
-    if (aDataset.mComponents.mIsChannelPresent)
+    if (dataset.mComponents.mIsChannelPresent)
     {
-        OutputLine("Channel: %d", aDataset.mChannel);
+        OutputLine("Channel: %d", dataset.mChannel);
     }
 
-    if (aDataset.mComponents.mIsChannelMaskPresent)
+    if (dataset.mComponents.mIsChannelMaskPresent)
     {
-        OutputLine("Channel Mask: 0x%08x", aDataset.mChannelMask);
+        OutputLine("Channel Mask: 0x%08lx", ToUlong(dataset.mChannelMask));
     }
 
-    if (aDataset.mComponents.mIsDelayPresent)
+    if (dataset.mComponents.mIsDelayPresent)
     {
-        OutputLine("Delay: %d", aDataset.mDelay);
+        OutputLine("Delay: %lu", ToUlong(dataset.mDelay));
     }
 
-    if (aDataset.mComponents.mIsExtendedPanIdPresent)
+    if (dataset.mComponents.mIsExtendedPanIdPresent)
     {
         OutputFormat("Ext PAN ID: ");
-        OutputBytesLine(aDataset.mExtendedPanId.m8);
+        OutputBytesLine(dataset.mExtendedPanId.m8);
     }
 
-    if (aDataset.mComponents.mIsMeshLocalPrefixPresent)
+    if (dataset.mComponents.mIsMeshLocalPrefixPresent)
     {
         OutputFormat("Mesh Local Prefix: ");
-        OutputIp6PrefixLine(aDataset.mMeshLocalPrefix);
+        OutputIp6PrefixLine(dataset.mMeshLocalPrefix);
     }
 
-    if (aDataset.mComponents.mIsNetworkKeyPresent)
+    if (dataset.mComponents.mIsNetworkKeyPresent)
     {
         OutputFormat("Network Key: ");
-        OutputBytesLine(aDataset.mNetworkKey.m8);
+        OutputBytesLine(dataset.mNetworkKey.m8);
     }
 
-    if (aDataset.mComponents.mIsNetworkNamePresent)
+    if (dataset.mComponents.mIsNetworkNamePresent)
     {
         OutputFormat("Network Name: ");
-        OutputLine("%s", aDataset.mNetworkName.m8);
+        OutputLine("%s", dataset.mNetworkName.m8);
     }
 
-    if (aDataset.mComponents.mIsPanIdPresent)
+    if (dataset.mComponents.mIsPanIdPresent)
     {
-        OutputLine("PAN ID: 0x%04x", aDataset.mPanId);
+        OutputLine("PAN ID: 0x%04x", dataset.mPanId);
     }
 
-    if (aDataset.mComponents.mIsPskcPresent)
+    if (dataset.mComponents.mIsPskcPresent)
     {
         OutputFormat("PSKc: ");
-        OutputBytesLine(aDataset.mPskc.m8);
+        OutputBytesLine(dataset.mPskc.m8);
     }
 
-    if (aDataset.mComponents.mIsSecurityPolicyPresent)
+    if (dataset.mComponents.mIsSecurityPolicyPresent)
     {
         OutputFormat("Security Policy: ");
-        OutputSecurityPolicy(aDataset.mSecurityPolicy);
-    }
-
-    return OT_ERROR_NONE;
-}
-
-template <> otError Dataset::Process<Cmd("init")>(Arg aArgs[])
-{
-    otError error = OT_ERROR_INVALID_ARGS;
-
-    if (aArgs[0] == "active")
-    {
-        error = otDatasetGetActive(GetInstancePtr(), &sDataset);
-    }
-    else if (aArgs[0] == "pending")
-    {
-        error = otDatasetGetPending(GetInstancePtr(), &sDataset);
-    }
-#if OPENTHREAD_FTD
-    else if (aArgs[0] == "new")
-    {
-        error = otDatasetCreateNewNetwork(GetInstancePtr(), &sDataset);
-    }
-#endif
-    else if (aArgs[0] == "tlvs")
-    {
-        otOperationalDatasetTlvs datasetTlvs;
-        uint16_t                 size = sizeof(datasetTlvs.mTlvs);
-
-        SuccessOrExit(error = aArgs[1].ParseAsHexString(size, datasetTlvs.mTlvs));
-        datasetTlvs.mLength = static_cast<uint8_t>(size);
-
-        SuccessOrExit(error = otDatasetParseTlvs(&datasetTlvs, &sDataset));
+        OutputSecurityPolicy(dataset.mSecurityPolicy);
     }
 
 exit:
     return error;
 }
 
-template <> otError Dataset::Process<Cmd("active")>(Arg aArgs[])
+/**
+ * @cli dataset init (active,new,pending,tlvs)
+ * @code
+ * dataset init new
+ * Done
+ * @endcode
+ * @cparam dataset init {@ca{active}|@ca{new}|@ca{pending}|@ca{tlvs}} [@ca{hex-encoded-tlvs}]
+ * Use `new` to initialize a new dataset, then enter the command `dataset commit active`.
+ * Use `tlvs` for hex-encoded TLVs.
+ * @par
+ * OT CLI checks for `active`, `pending`, or `tlvs` and returns the corresponding values. Otherwise,
+ * OT CLI creates a new, random network and returns a new dataset.
+ * @csa{dataset commit active}
+ * @csa{dataset active}
+ */
+template <> otError Dataset::Process<Cmd("init")>(Arg aArgs[])
 {
     otError error = OT_ERROR_INVALID_ARGS;
 
-    if (aArgs[0].IsEmpty())
+    if (aArgs[0] == "active")
+    {
+        error = otDatasetGetActiveTlvs(GetInstancePtr(), &sDatasetTlvs);
+    }
+    else if (aArgs[0] == "pending")
+    {
+        error = otDatasetGetPendingTlvs(GetInstancePtr(), &sDatasetTlvs);
+    }
+#if OPENTHREAD_FTD
+    else if (aArgs[0] == "new")
     {
         otOperationalDataset dataset;
 
-        SuccessOrExit(error = otDatasetGetActive(GetInstancePtr(), &dataset));
+        SuccessOrExit(error = otDatasetCreateNewNetwork(GetInstancePtr(), &dataset));
+        SuccessOrExit(error = otDatasetConvertToTlvs(&dataset, &sDatasetTlvs));
+    }
+#endif
+    else if (aArgs[0] == "tlvs")
+    {
+        uint16_t size = sizeof(sDatasetTlvs.mTlvs);
+
+        SuccessOrExit(error = aArgs[1].ParseAsHexString(size, sDatasetTlvs.mTlvs));
+        sDatasetTlvs.mLength = static_cast<uint8_t>(size);
+    }
+
+exit:
+    return error;
+}
+
+/**
+ * @cli dataset active
+ * @code
+ * dataset active
+ * Active Timestamp: 1
+ * Channel: 13
+ * Channel Mask: 0x07fff800
+ * Ext PAN ID: d63e8e3e495ebbc3
+ * Mesh Local Prefix: fd3d:b50b:f96d:722d::/64
+ * Network Key: dfd34f0f05cad978ec4e32b0413038ff
+ * Network Name: OpenThread-8f28
+ * PAN ID: 0x8f28
+ * PSKc: c23a76e98f1a6483639b1ac1271e2e27
+ * Security Policy: 0, onrcb
+ * Done
+ * @endcode
+ * @code
+ * dataset active -x
+ * 0e08000000000001000000030000103506000...3023d82c841eff0e68db86f35740c030000ff
+ * Done
+ * @endcode
+ * @cparam dataset active [-x]
+ * The optional `-x` argument prints the Active Operational %Dataset values as hex-encoded TLVs.
+ * @par api_copy
+ * #otDatasetGetActive
+ * @par
+ * OT CLI uses #otOperationalDataset members to return dataset values to the console.
+ */
+template <> otError Dataset::Process<Cmd("active")>(Arg aArgs[])
+{
+    otError                  error;
+    otOperationalDatasetTlvs dataset;
+
+    SuccessOrExit(error = otDatasetGetActiveTlvs(GetInstancePtr(), &dataset));
+
+    if (aArgs[0].IsEmpty())
+    {
         error = Print(dataset);
     }
     else if (aArgs[0] == "-x")
     {
-        otOperationalDatasetTlvs dataset;
-
-        SuccessOrExit(error = otDatasetGetActiveTlvs(GetInstancePtr(), &dataset));
         OutputBytesLine(dataset.mTlvs, dataset.mLength);
     }
+    else
+    {
+        error = OT_ERROR_INVALID_ARGS;
+    }
 
 exit:
     return error;
@@ -176,97 +228,167 @@
 
 template <> otError Dataset::Process<Cmd("pending")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_INVALID_ARGS;
+    otError                  error;
+    otOperationalDatasetTlvs datasetTlvs;
+
+    SuccessOrExit(error = otDatasetGetPendingTlvs(GetInstancePtr(), &datasetTlvs));
 
     if (aArgs[0].IsEmpty())
     {
-        otOperationalDataset dataset;
-
-        SuccessOrExit(error = otDatasetGetPending(GetInstancePtr(), &dataset));
-        error = Print(dataset);
+        error = Print(datasetTlvs);
     }
     else if (aArgs[0] == "-x")
     {
-        otOperationalDatasetTlvs dataset;
-
-        SuccessOrExit(error = otDatasetGetPendingTlvs(GetInstancePtr(), &dataset));
-        OutputBytesLine(dataset.mTlvs, dataset.mLength);
+        OutputBytesLine(datasetTlvs.mTlvs, datasetTlvs.mLength);
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_ARGS;
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset activetimestamp (get, set)
+ * @code
+ * dataset activetimestamp
+ * 123456789
+ * Done
+ * @endcode
+ * @code
+ * dataset activetimestamp 123456789
+ * Done
+ * @endcode
+ * @cparam dataset activetimestamp [@ca{timestamp}]
+ * Pass the optional `timestamp` argument to set the active timestamp.
+ * @par
+ * Gets or sets #otOperationalDataset::mActiveTimestamp.
+ */
 template <> otError Dataset::Process<Cmd("activetimestamp")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsActiveTimestampPresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsActiveTimestampPresent)
         {
-            OutputLine("%lu", sDataset.mActiveTimestamp.mSeconds);
+            OutputUint64Line(dataset.mActiveTimestamp.mSeconds);
         }
     }
     else
     {
-        SuccessOrExit(error = aArgs[0].ParseAsUint64(sDataset.mActiveTimestamp.mSeconds));
-        sDataset.mActiveTimestamp.mTicks               = 0;
-        sDataset.mActiveTimestamp.mAuthoritative       = false;
-        sDataset.mComponents.mIsActiveTimestampPresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        SuccessOrExit(error = aArgs[0].ParseAsUint64(dataset.mActiveTimestamp.mSeconds));
+        dataset.mActiveTimestamp.mTicks               = 0;
+        dataset.mActiveTimestamp.mAuthoritative       = false;
+        dataset.mComponents.mIsActiveTimestampPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset channel (get,set)
+ * @code
+ * dataset channel
+ * 12
+ * Done
+ * @endcode
+ * @code
+ * dataset channel 12
+ * Done
+ * @endcode
+ * @cparam dataset channel [@ca{channel-num}]
+ * Use the optional `channel-num` argument to set the channel.
+ * @par
+ * Gets or sets #otOperationalDataset::mChannel.
+ */
 template <> otError Dataset::Process<Cmd("channel")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsChannelPresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsChannelPresent)
         {
-            OutputLine("%d", sDataset.mChannel);
+            OutputLine("%d", dataset.mChannel);
         }
     }
     else
     {
-        SuccessOrExit(error = aArgs[0].ParseAsUint16(sDataset.mChannel));
-        sDataset.mComponents.mIsChannelPresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        SuccessOrExit(error = aArgs[0].ParseAsUint16(dataset.mChannel));
+        dataset.mComponents.mIsChannelPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset channelmask (get,set)
+ * @code
+ * dataset channelmask
+ * 0x07fff800
+ * Done
+ * @endcode
+ * @code
+ * dataset channelmask 0x07fff800
+ * Done
+ * @endcode
+ * @cparam dataset channelmask [@ca{channel-mask}]
+ * Use the optional `channel-mask` argument to set the channel mask.
+ * @par
+ * Gets or sets #otOperationalDataset::mChannelMask
+ */
 template <> otError Dataset::Process<Cmd("channelmask")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsChannelMaskPresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsChannelMaskPresent)
         {
-            OutputLine("0x%08x", sDataset.mChannelMask);
+            OutputLine("0x%08lx", ToUlong(dataset.mChannelMask));
         }
     }
     else
     {
-        SuccessOrExit(error = aArgs[0].ParseAsUint32(sDataset.mChannelMask));
-        sDataset.mComponents.mIsChannelMaskPresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        SuccessOrExit(error = aArgs[0].ParseAsUint32(dataset.mChannelMask));
+        dataset.mComponents.mIsChannelMaskPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset clear
+ * @code
+ * dataset clear
+ * Done
+ * @endcode
+ * @par
+ * Reset the Operational %Dataset buffer.
+ */
 template <> otError Dataset::Process<Cmd("clear")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
 
-    memset(&sDataset, 0, sizeof(sDataset));
+    memset(&sDatasetTlvs, 0, sizeof(sDatasetTlvs));
     return OT_ERROR_NONE;
 }
 
@@ -274,70 +396,154 @@
 {
     otError error = OT_ERROR_INVALID_ARGS;
 
+    /**
+     * @cli dataset commit active
+     * @code
+     * dataset commit active
+     * Done
+     * @endcode
+     * @par
+     * Commit the Operational %Dataset buffer to Active Operational %Dataset.
+     * @csa{dataset commit pending}
+     * @sa #otDatasetSetPending
+     */
     if (aArgs[0] == "active")
     {
-        error = otDatasetSetActive(GetInstancePtr(), &sDataset);
+        error = otDatasetSetActiveTlvs(GetInstancePtr(), &sDatasetTlvs);
     }
+    /**
+     * @cli dataset commit pending
+     * @code
+     * dataset commit pending
+     * Done
+     * @endcode
+     * @par
+     * Commit the Operational %Dataset buffer to Pending Operational %Dataset.
+     * @csa{dataset commit active}
+     * @sa #otDatasetSetActive
+     */
     else if (aArgs[0] == "pending")
     {
-        error = otDatasetSetPending(GetInstancePtr(), &sDataset);
+        error = otDatasetSetPendingTlvs(GetInstancePtr(), &sDatasetTlvs);
     }
 
     return error;
 }
 
+/**
+ * @cli dataset delay (get,set)
+ * @code
+ * dataset delay
+ * 1000
+ * Done
+ * @endcode
+ * @code
+ * dataset delay 1000
+ * Done
+ * @endcode
+ * @cparam dataset delay [@ca{delay}]
+ * Use the optional `delay` argument to set the delay timer value.
+ * @par
+ * Gets or sets #otOperationalDataset::mDelay.
+ * @sa otDatasetSetDelayTimerMinimal
+ */
 template <> otError Dataset::Process<Cmd("delay")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsDelayPresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsDelayPresent)
         {
-            OutputLine("%d", sDataset.mDelay);
+            OutputLine("%lu", ToUlong(dataset.mDelay));
         }
     }
     else
     {
-        SuccessOrExit(error = aArgs[0].ParseAsUint32(sDataset.mDelay));
-        sDataset.mComponents.mIsDelayPresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        SuccessOrExit(error = aArgs[0].ParseAsUint32(dataset.mDelay));
+        dataset.mComponents.mIsDelayPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset extpanid (get,set)
+ * @code
+ * dataset extpanid
+ * 000db80123456789
+ * Done
+ * @endcode
+ * @code
+ * dataset extpanid 000db80123456789
+ * Done
+ * @endcode
+ * @cparam dataset extpanid [@ca{extpanid}]
+ * Use the optional `extpanid` argument to set the Extended Personal Area Network ID.
+ * @par
+ * Gets or sets #otOperationalDataset::mExtendedPanId.
+ * @note The commissioning credential in the dataset buffer becomes stale after changing
+ * this value. Use `dataset pskc` to reset.
+ * @csa{dataset pskc (get,set)}
+ */
 template <> otError Dataset::Process<Cmd("extpanid")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsExtendedPanIdPresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsExtendedPanIdPresent)
         {
-            OutputBytesLine(sDataset.mExtendedPanId.m8);
+            OutputBytesLine(dataset.mExtendedPanId.m8);
         }
     }
     else
     {
-        SuccessOrExit(error = aArgs[0].ParseAsHexString(sDataset.mExtendedPanId.m8));
-        sDataset.mComponents.mIsExtendedPanIdPresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        SuccessOrExit(error = aArgs[0].ParseAsHexString(dataset.mExtendedPanId.m8));
+        dataset.mComponents.mIsExtendedPanIdPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset meshlocalprefix (get,set)
+ * @code
+ * dataset meshlocalprefix
+ * fd00:db8:0:0::/64
+ * Done
+ * @endcode
+ * @code
+ * dataset meshlocalprefix fd00:db8:0:0::/64
+ * Done
+ * @endcode
+ * @cparam dataset meshlocalprefix [@ca{meshlocalprefix}]
+ * Use the optional `meshlocalprefix` argument to set the Mesh-Local Prefix.
+ * @par
+ * Gets or sets #otOperationalDataset::mMeshLocalPrefix.
+ */
 template <> otError Dataset::Process<Cmd("meshlocalprefix")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsMeshLocalPrefixPresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsMeshLocalPrefixPresent)
         {
             OutputFormat("Mesh Local Prefix: ");
-            OutputIp6PrefixLine(sDataset.mMeshLocalPrefix);
+            OutputIp6PrefixLine(dataset.mMeshLocalPrefix);
         }
     }
     else
@@ -346,94 +552,179 @@
 
         SuccessOrExit(error = aArgs[0].ParseAsIp6Address(prefix));
 
-        memcpy(sDataset.mMeshLocalPrefix.m8, prefix.mFields.m8, sizeof(sDataset.mMeshLocalPrefix.m8));
-        sDataset.mComponents.mIsMeshLocalPrefixPresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        memcpy(dataset.mMeshLocalPrefix.m8, prefix.mFields.m8, sizeof(dataset.mMeshLocalPrefix.m8));
+        dataset.mComponents.mIsMeshLocalPrefixPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset networkkey (get,set)
+ * @code
+ * dataset networkkey
+ * 00112233445566778899aabbccddeeff
+ * Done
+ * @endcode
+ * @code
+ * dataset networkkey 00112233445566778899aabbccddeeff
+ * Done
+ * @endcode
+ * @cparam dataset networkkey [@ca{key}]
+ * Use the optional `key` argument to set the Network Key.
+ * @par
+ * Gets or sets #otOperationalDataset::mNetworkKey.
+ */
 template <> otError Dataset::Process<Cmd("networkkey")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsNetworkKeyPresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsNetworkKeyPresent)
         {
-            OutputBytesLine(sDataset.mNetworkKey.m8);
+            OutputBytesLine(dataset.mNetworkKey.m8);
         }
     }
     else
     {
-        SuccessOrExit(error = aArgs[0].ParseAsHexString(sDataset.mNetworkKey.m8));
-        sDataset.mComponents.mIsNetworkKeyPresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        SuccessOrExit(error = aArgs[0].ParseAsHexString(dataset.mNetworkKey.m8));
+        dataset.mComponents.mIsNetworkKeyPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset networkname (get,set)
+ * @code
+ * dataset networkname
+ * OpenThread
+ * Done
+ * @endcode
+ * @code
+ * dataset networkname OpenThread
+ * Done
+ * @endcode
+ * @cparam dataset networkname [@ca{name}]
+ * Use the optional `name` argument to set the Network Name.
+ * @par
+ * Gets or sets #otOperationalDataset::mNetworkName.
+ * @note The Commissioning Credential in the dataset buffer becomes stale after changing this value.
+ * Use `dataset pskc` to reset.
+ * @csa{dataset pskc (get,set)}
+ */
 template <> otError Dataset::Process<Cmd("networkname")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsNetworkNamePresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsNetworkNamePresent)
         {
-            OutputLine("%s", sDataset.mNetworkName.m8);
+            OutputLine("%s", dataset.mNetworkName.m8);
         }
     }
     else
     {
-        SuccessOrExit(error = otNetworkNameFromString(&sDataset.mNetworkName, aArgs[0].GetCString()));
-        sDataset.mComponents.mIsNetworkNamePresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        SuccessOrExit(error = otNetworkNameFromString(&dataset.mNetworkName, aArgs[0].GetCString()));
+        dataset.mComponents.mIsNetworkNamePresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset panid (get,set)
+ * @code
+ * dataset panid
+ * 0x1234
+ * Done
+ * @endcode
+ * @code
+ * dataset panid 0x1234
+ * Done
+ * @endcode
+ * @cparam dataset panid [@ca{panid}]
+ * Use the optional `panid` argument to set the PAN ID.
+ * @par
+ * Gets or sets #otOperationalDataset::mPanId.
+ */
 template <> otError Dataset::Process<Cmd("panid")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsPanIdPresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsPanIdPresent)
         {
-            OutputLine("0x%04x", sDataset.mPanId);
+            OutputLine("0x%04x", dataset.mPanId);
         }
     }
     else
     {
-        SuccessOrExit(error = aArgs[0].ParseAsUint16(sDataset.mPanId));
-        sDataset.mComponents.mIsPanIdPresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        SuccessOrExit(error = aArgs[0].ParseAsUint16(dataset.mPanId));
+        dataset.mComponents.mIsPanIdPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset pendingtimestamp (get,set)
+ * @code
+ * dataset pendingtimestamp
+ * 123456789
+ * Done
+ * @endcode
+ * @code
+ * dataset pendingtimestamp 123456789
+ * Done
+ * @endcode
+ * @cparam dataset pendingtimestamp [@ca{timestamp}]
+ * Use the optional `timestamp` argument to set the pending timestamp seconds.
+ * @par
+ * Gets or sets #otOperationalDataset::mPendingTimestamp.
+ */
 template <> otError Dataset::Process<Cmd("pendingtimestamp")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsPendingTimestampPresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsPendingTimestampPresent)
         {
-            OutputLine("%lu", sDataset.mPendingTimestamp.mSeconds);
+            OutputUint64Line(dataset.mPendingTimestamp.mSeconds);
         }
     }
     else
     {
-        SuccessOrExit(error = aArgs[0].ParseAsUint64(sDataset.mPendingTimestamp.mSeconds));
-        sDataset.mPendingTimestamp.mTicks               = 0;
-        sDataset.mPendingTimestamp.mAuthoritative       = false;
-        sDataset.mComponents.mIsPendingTimestampPresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        SuccessOrExit(error = aArgs[0].ParseAsUint64(dataset.mPendingTimestamp.mSeconds));
+        dataset.mPendingTimestamp.mTicks               = 0;
+        dataset.mPendingTimestamp.mAuthoritative       = false;
+        dataset.mComponents.mIsPendingTimestampPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
@@ -540,11 +831,43 @@
         }
     }
 
+    /**
+     * @cli dataset mgmtsetcommand active
+     * @code
+     * dataset mgmtsetcommand active activetimestamp 123 securitypolicy 1 onrcb
+     * Done
+     * @endcode
+     * @cparam dataset mgmtsetcommand active [@ca{dataset-components}] [-x @ca{tlv-list}]
+     * To learn more about these parameters and argument mappings, refer to @dataset.
+     * @par
+     * @note This command is primarily used for testing only.
+     * @par api_copy
+     * #otDatasetSendMgmtActiveSet
+     * @csa{dataset mgmtgetcommand active}
+     * @csa{dataset mgmtgetcommand pending}
+     * @csa{dataset mgmtsetcommand pending}
+     */
     if (aArgs[0] == "active")
     {
         error = otDatasetSendMgmtActiveSet(GetInstancePtr(), &dataset, tlvs, tlvsLength, /* aCallback */ nullptr,
                                            /* aContext */ nullptr);
     }
+    /**
+     * @cli dataset mgmtsetcommand pending
+     * @code
+     * dataset mgmtsetcommand pending activetimestamp 123 securitypolicy 1 onrcb
+     * Done
+     * @endcode
+     * @cparam dataset mgmtsetcommand pending [@ca{dataset-components}] [-x @ca{tlv-list}]
+     * To learn more about these parameters and argument mappings, refer to @dataset.
+     * @par
+     * @note This command is primarily used for testing only.
+     * @par api_copy
+     * #otDatasetSendMgmtPendingSet
+     * @csa{dataset mgmtgetcommand active}
+     * @csa{dataset mgmtgetcommand pending}
+     * @csa{dataset mgmtsetcommand active}
+     */
     else if (aArgs[0] == "pending")
     {
         error = otDatasetSendMgmtPendingSet(GetInstancePtr(), &dataset, tlvs, tlvsLength, /* aCallback */ nullptr,
@@ -633,11 +956,56 @@
         }
     }
 
+    /**
+     * @cli dataset mgmtgetcommand active
+     * @code
+     * dataset mgmtgetcommand active address fdde:ad00:beef:0:558:f56b:d688:799 activetimestamp securitypolicy
+     * Done
+     * @endcode
+     * @code
+     * dataset mgmtgetcommand active networkname
+     * Done
+     * @endcode
+     * @cparam dataset mgmtgetcommand active [address @ca{leader-address}] [@ca{dataset-components}] [-x @ca{tlv-list}]
+     * *    Use `address` to specify the IPv6 destination; otherwise, the Leader ALOC is used as default.
+     * *    For `dataset-components`, you can pass any combination of #otOperationalDatasetComponents, for
+     *      example `activetimestamp`, `pendingtimestamp`, or `networkkey`.
+     * *    The optional `-x` argument specifies raw TLVs to be requested.
+     * @par
+     * OT CLI sends a MGMT_ACTIVE_GET with the relevant arguments.
+     * To learn more about these parameters and argument mappings, refer to @dataset.
+     * @note This command is primarily used for testing only.
+     * @par api_copy
+     * #otDatasetSendMgmtActiveGet
+     * @csa{dataset mgmtgetcommand pending}
+     * @csa{dataset mgmtsetcommand active}
+     * @csa{dataset mgmtsetcommand pending}
+     */
     if (aArgs[0] == "active")
     {
         error = otDatasetSendMgmtActiveGet(GetInstancePtr(), &datasetComponents, tlvs, tlvsLength,
                                            destAddrSpecified ? &address : nullptr);
     }
+    /**
+     * @cli dataset mgmtgetcommand pending
+     * @code
+     * dataset mgmtgetcommand pending address fdde:ad00:beef:0:558:f56b:d688:799 activetimestamp securitypolicy
+     * Done
+     * @endcode
+     * @code
+     * dataset mgmtgetcommand pending networkname
+     * Done
+     * @endcode
+     * @cparam dataset mgmtgetcommand pending [address @ca{leader-address}] [@ca{dataset-components}] [-x @ca{tlv-list}]
+     * To learn more about these parameters and argument mappings, refer to @dataset.
+     * @par
+     * @note This command is primarily used for testing only.
+     * @par api_copy
+     * #otDatasetSendMgmtPendingGet
+     * @csa{dataset mgmtgetcommand active}
+     * @csa{dataset mgmtsetcommand active}
+     * @csa{dataset mgmtsetcommand pending}
+     */
     else if (aArgs[0] == "pending")
     {
         error = otDatasetSendMgmtPendingGet(GetInstancePtr(), &datasetComponents, tlvs, tlvsLength,
@@ -652,21 +1020,48 @@
     return error;
 }
 
+/**
+ * @cli dataset pskc (get,set)
+ * @code
+ * dataset pskc
+ * 67c0c203aa0b042bfb5381c47aef4d9e
+ * Done
+ * @endcode
+ * @code
+ * dataset pskc -p 123456
+ * Done
+ * @endcode
+ * @code
+ * dataset pskc 67c0c203aa0b042bfb5381c47aef4d9e
+ * Done
+ * @endcode
+ * @cparam dataset pskc [@ca{-p} @ca{passphrase}] | [@ca{key}]
+ * For FTD only, use `-p` with the `passphrase` argument. `-p` generates a pskc from
+ * the UTF-8 encoded `passphrase` that you provide, together with
+ * the network name and extended PAN ID. If set, `-p` uses the dataset buffer;
+ * otherwise, it uses the current stack.
+ * Alternatively, you can set pskc as `key` (hex format).
+ * @par
+ * Gets or sets #otOperationalDataset::mPskc.
+ */
 template <> otError Dataset::Process<Cmd("pskc")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        // sDataset holds the key as a literal string, we don't
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        // dataset holds the key as a literal string, we don't
         // need to export it from PSA ITS.
-        if (sDataset.mComponents.mIsPskcPresent)
+        if (dataset.mComponents.mIsPskcPresent)
         {
-            OutputBytesLine(sDataset.mPskc.m8);
+            OutputBytesLine(dataset.mPskc.m8);
         }
     }
     else
     {
+        memset(&dataset, 0, sizeof(dataset));
 #if OPENTHREAD_FTD
         if (aArgs[0] == "-p")
         {
@@ -675,20 +1070,21 @@
             SuccessOrExit(
                 error = otDatasetGeneratePskc(
                     aArgs[1].GetCString(),
-                    (sDataset.mComponents.mIsNetworkNamePresent
-                         ? &sDataset.mNetworkName
+                    (dataset.mComponents.mIsNetworkNamePresent
+                         ? &dataset.mNetworkName
                          : reinterpret_cast<const otNetworkName *>(otThreadGetNetworkName(GetInstancePtr()))),
-                    (sDataset.mComponents.mIsExtendedPanIdPresent ? &sDataset.mExtendedPanId
-                                                                  : otThreadGetExtendedPanId(GetInstancePtr())),
-                    &sDataset.mPskc));
+                    (dataset.mComponents.mIsExtendedPanIdPresent ? &dataset.mExtendedPanId
+                                                                 : otThreadGetExtendedPanId(GetInstancePtr())),
+                    &dataset.mPskc));
         }
         else
 #endif
         {
-            SuccessOrExit(error = aArgs[0].ParseAsHexString(sDataset.mPskc.m8));
+            SuccessOrExit(error = aArgs[0].ParseAsHexString(dataset.mPskc.m8));
         }
 
-        sDataset.mComponents.mIsPskcPresent = true;
+        dataset.mComponents.mIsPskcPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
@@ -739,7 +1135,7 @@
         OutputFormat("R");
     }
 
-    OutputLine("");
+    OutputNewLine();
 }
 
 otError Dataset::ParseSecurityPolicy(otSecurityPolicy &aSecurityPolicy, Arg *&aArgs)
@@ -806,29 +1202,69 @@
     return error;
 }
 
+/**
+ * @cli dataset securitypolicy (get,set)
+ * @code
+ * dataset securitypolicy
+ * 672 onrc
+ * Done
+ * @endcode
+ * @code
+ * dataset securitypolicy 672 onrc
+ * Done
+ * @endcode
+ * @cparam dataset securitypolicy [@ca{rotationtime} [@ca{onrcCepR}]]
+ * *   Use `rotationtime` for `thrKeyRotation`, in units of hours.
+ * *   Security Policy commands use the `onrcCepR` argument mappings to get and set
+ * #otSecurityPolicy members, for example `o` represents
+ * #otSecurityPolicy::mObtainNetworkKeyEnabled.
+ * @moreinfo{@dataset}.
+ * @par
+ * Gets or sets the %Dataset security policy.
+ */
 template <> otError Dataset::Process<Cmd("securitypolicy")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
+    otError              error = OT_ERROR_NONE;
+    otOperationalDataset dataset;
 
     if (aArgs[0].IsEmpty())
     {
-        if (sDataset.mComponents.mIsSecurityPolicyPresent)
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        if (dataset.mComponents.mIsSecurityPolicyPresent)
         {
-            OutputSecurityPolicy(sDataset.mSecurityPolicy);
+            OutputSecurityPolicy(dataset.mSecurityPolicy);
         }
     }
     else
     {
         Arg *arg = &aArgs[0];
 
-        SuccessOrExit(error = ParseSecurityPolicy(sDataset.mSecurityPolicy, arg));
-        sDataset.mComponents.mIsSecurityPolicyPresent = true;
+        memset(&dataset, 0, sizeof(dataset));
+        SuccessOrExit(error = ParseSecurityPolicy(dataset.mSecurityPolicy, arg));
+        dataset.mComponents.mIsSecurityPolicyPresent = true;
+        SuccessOrExit(error = otDatasetUpdateTlvs(&dataset, &sDatasetTlvs));
     }
 
 exit:
     return error;
 }
 
+/**
+ * @cli dataset set (active,pending)
+ * @code
+ * dataset set active 0e08000000000001000000030000103506000...3023d82c841eff0e68db86f35740c030000ff
+ * Done
+ * @endcode
+ * @code
+ * dataset set pending 0e08000000000001000000030000103506000...3023d82c841eff0e68db86f35740c030000ff
+ * Done
+ * @endcode
+ * @cparam dataset set {active|pending} @ca{tlvs}
+ * @par
+ * The CLI `dataset set` command sets the Active Operational %Dataset using hex-encoded TLVs.
+ * @par api_copy
+ * #otDatasetSetActive
+ */
 template <> otError Dataset::Process<Cmd("set")>(Arg aArgs[])
 {
     otError                error = OT_ERROR_NONE;
@@ -848,22 +1284,22 @@
     }
 
     {
-        MeshCoP::Dataset       dataset;
-        MeshCoP::Dataset::Info datasetInfo;
-        uint16_t               tlvsLength = MeshCoP::Dataset::kMaxSize;
+        otOperationalDataset     dataset;
+        otOperationalDatasetTlvs datasetTlvs;
+        uint16_t                 tlvsLength = MeshCoP::Dataset::kMaxSize;
 
-        SuccessOrExit(error = aArgs[1].ParseAsHexString(tlvsLength, dataset.GetBytes()));
-        dataset.SetSize(tlvsLength);
-        VerifyOrExit(dataset.IsValid(), error = OT_ERROR_INVALID_ARGS);
-        dataset.ConvertTo(datasetInfo);
+        SuccessOrExit(error = aArgs[1].ParseAsHexString(tlvsLength, datasetTlvs.mTlvs));
+        datasetTlvs.mLength = static_cast<uint8_t>(tlvsLength);
+
+        SuccessOrExit(error = otDatasetParseTlvs(&datasetTlvs, &dataset));
 
         switch (datasetType)
         {
         case MeshCoP::Dataset::Type::kActive:
-            SuccessOrExit(error = otDatasetSetActive(GetInstancePtr(), &datasetInfo));
+            SuccessOrExit(error = otDatasetSetActiveTlvs(GetInstancePtr(), &datasetTlvs));
             break;
         case MeshCoP::Dataset::Type::kPending:
-            SuccessOrExit(error = otDatasetSetPending(GetInstancePtr(), &datasetInfo));
+            SuccessOrExit(error = otDatasetSetPendingTlvs(GetInstancePtr(), &datasetTlvs));
             break;
         }
     }
@@ -872,6 +1308,27 @@
     return error;
 }
 
+/**
+ * @cli dataset tlvs
+ * @code
+ * dataset tlvs
+ * 0e080000000000010000000300001635060004001fffe0020...f7f8
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otDatasetConvertToTlvs
+ */
+template <> otError Dataset::Process<Cmd("tlvs")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    OutputBytesLine(sDatasetTlvs.mTlvs, sDatasetTlvs.mLength);
+
+exit:
+    return error;
+}
+
 #if OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE && OPENTHREAD_FTD
 
 template <> otError Dataset::Process<Cmd("updater")>(Arg aArgs[])
@@ -884,7 +1341,11 @@
     }
     else if (aArgs[0] == "start")
     {
-        error = otDatasetUpdaterRequestUpdate(GetInstancePtr(), &sDataset, &Dataset::HandleDatasetUpdater, this);
+        otOperationalDataset dataset;
+
+        SuccessOrExit(error = otDatasetParseTlvs(&sDatasetTlvs, &dataset));
+        SuccessOrExit(
+            error = otDatasetUpdaterRequestUpdate(GetInstancePtr(), &dataset, &Dataset::HandleDatasetUpdater, this));
     }
     else if (aArgs[0] == "cancel")
     {
@@ -895,6 +1356,7 @@
         error = OT_ERROR_INVALID_ARGS;
     }
 
+exit:
     return error;
 }
 
@@ -938,6 +1400,7 @@
         CmdEntry("pskc"),
         CmdEntry("securitypolicy"),
         CmdEntry("set"),
+        CmdEntry("tlvs"),
 #if OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE && OPENTHREAD_FTD
         CmdEntry("updater"),
 #endif
@@ -952,9 +1415,40 @@
 
     if (aArgs[0].IsEmpty())
     {
-        ExitNow(error = Print(sDataset));
+        ExitNow(error = Print(sDatasetTlvs));
     }
 
+    /**
+     * @cli dataset help
+     * @code
+     * dataset help
+     * help
+     * active
+     * activetimestamp
+     * channel
+     * channelmask
+     * clear
+     * commit
+     * delay
+     * extpanid
+     * init
+     * meshlocalprefix
+     * mgmtgetcommand
+     * mgmtsetcommand
+     * networkkey
+     * networkname
+     * panid
+     * pending
+     * pendingtimestamp
+     * pskc
+     * securitypolicy
+     * set
+     * tlvs
+     * Done
+     * @endcode
+     * @par
+     * Gets a list of `dataset` CLI commands. @moreinfo{@dataset}.
+     */
     if (aArgs[0] == "help")
     {
         OutputCommandTable(kCommands);
diff --git a/src/cli/cli_dataset.hpp b/src/cli/cli_dataset.hpp
index 980bb5d..7f11ec3 100644
--- a/src/cli/cli_dataset.hpp
+++ b/src/cli/cli_dataset.hpp
@@ -49,13 +49,13 @@
  * This class implements the Dataset CLI interpreter.
  *
  */
-class Dataset : private OutputWrapper
+class Dataset : private Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg;
 
-    explicit Dataset(Output &aOutput)
-        : OutputWrapper(aOutput)
+    Dataset(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+        : Output(aInstance, aOutputImplementer)
     {
     }
 
@@ -72,7 +72,7 @@
 
     template <CommandId kCommandId> otError Process(Arg aArgs[]);
 
-    otError Print(otOperationalDataset &aDataset);
+    otError Print(otOperationalDatasetTlvs &aDatasetTlvs);
 
 #if OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE && OPENTHREAD_FTD
     otError     ProcessUpdater(Arg aArgs[]);
@@ -83,7 +83,7 @@
     void    OutputSecurityPolicy(const otSecurityPolicy &aSecurityPolicy);
     otError ParseSecurityPolicy(otSecurityPolicy &aSecurityPolicy, Arg *&aArgs);
 
-    static otOperationalDataset sDataset;
+    static otOperationalDatasetTlvs sDatasetTlvs;
 };
 
 } // namespace Cli
diff --git a/src/cli/cli_history.cpp b/src/cli/cli_history.cpp
index 3dc856b..0ea5ebf 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
 };
 
@@ -256,6 +256,92 @@
     return error;
 }
 
+template <> otError History::Process<Cmd("router")>(Arg aArgs[])
+{
+    static const char *const kEventString[] = {
+        /* (0) OT_HISTORY_TRACKER_ROUTER_EVENT_ADDED             -> */ "Added",
+        /* (1) OT_HISTORY_TRACKER_ROUTER_EVENT_REMOVED           -> */ "Removed",
+        /* (2) OT_HISTORY_TRACKER_ROUTER_EVENT_NEXT_HOP_CHANGED  -> */ "NextHopChanged",
+        /* (3) OT_HISTORY_TRACKER_ROUTER_EVENT_COST_CHANGED      -> */ "CostChanged",
+    };
+
+    constexpr uint8_t kRouterIdOffset = 10; // Bit offset of Router ID in RLOC16
+
+    otError                           error;
+    bool                              isList;
+    uint16_t                          numEntries;
+    otHistoryTrackerIterator          iterator;
+    const otHistoryTrackerRouterInfo *info;
+    uint32_t                          entryAge;
+    char                              ageString[OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE];
+
+    static_assert(0 == OT_HISTORY_TRACKER_ROUTER_EVENT_ADDED, "EVENT_ADDED is incorrect");
+    static_assert(1 == OT_HISTORY_TRACKER_ROUTER_EVENT_REMOVED, "EVENT_REMOVED is incorrect");
+    static_assert(2 == OT_HISTORY_TRACKER_ROUTER_EVENT_NEXT_HOP_CHANGED, "EVENT_NEXT_HOP_CHANGED is incorrect");
+    static_assert(3 == OT_HISTORY_TRACKER_ROUTER_EVENT_COST_CHANGED, "EVENT_COST_CHANGED is incorrect");
+
+    SuccessOrExit(error = ParseArgs(aArgs, isList, numEntries));
+
+    if (!isList)
+    {
+        // | Age                  | Event          | ID (RlOC16) | Next Hop   | Path Cost   |
+        // +----------------------+----------------+-------------+------------+-------------+
+
+        static const char *const kRouterInfoTitles[] = {
+            "Age", "Event", "ID (RLOC16)", "Next Hop", "Path Cost",
+        };
+
+        static const uint8_t kRouterInfoColumnWidths[] = {22, 16, 13, 13, 12};
+
+        OutputTableHeader(kRouterInfoTitles, kRouterInfoColumnWidths);
+    }
+
+    otHistoryTrackerInitIterator(&iterator);
+
+    for (uint16_t index = 0; (numEntries == 0) || (index < numEntries); index++)
+    {
+        info = otHistoryTrackerIterateRouterHistory(GetInstancePtr(), &iterator, &entryAge);
+        VerifyOrExit(info != nullptr);
+
+        otHistoryTrackerEntryAgeToString(entryAge, ageString, sizeof(ageString));
+
+        OutputFormat(isList ? "%s -> event:%s router:%u(0x%04x) nexthop:" : "| %20s | %-14s | %2u (0x%04x) | ",
+                     ageString, kEventString[info->mEvent], info->mRouterId,
+                     static_cast<uint16_t>(info->mRouterId) << kRouterIdOffset);
+
+        if (info->mNextHop != OT_HISTORY_TRACKER_NO_NEXT_HOP)
+        {
+            OutputFormat(isList ? "%u(0x%04x)" : "%2u (0x%04x)", info->mNextHop,
+                         static_cast<uint16_t>(info->mNextHop) << kRouterIdOffset);
+        }
+        else
+        {
+            OutputFormat(isList ? "%s" : "%11s", "none");
+        }
+
+        if (info->mOldPathCost != OT_HISTORY_TRACKER_INFINITE_PATH_COST)
+        {
+            OutputFormat(isList ? " old-cost:%u" : " | %3u ->", info->mOldPathCost);
+        }
+        else
+        {
+            OutputFormat(isList ? " old-cost:inf" : " | inf ->");
+        }
+
+        if (info->mPathCost != OT_HISTORY_TRACKER_INFINITE_PATH_COST)
+        {
+            OutputLine(isList ? " new-cost:%u" : " %3u |", info->mPathCost);
+        }
+        else
+        {
+            OutputLine(isList ? " new-cost:inf" : " inf |");
+        }
+    }
+
+exit:
+    return error;
+}
+
 template <> otError History::Process<Cmd("netinfo")>(Arg aArgs[])
 {
     otError                            error;
@@ -299,20 +385,11 @@
     return error;
 }
 
-template <> otError History::Process<Cmd("rx")>(Arg aArgs[])
-{
-    return ProcessRxTxHistory(kRx, aArgs);
-}
+template <> otError History::Process<Cmd("rx")>(Arg aArgs[]) { return ProcessRxTxHistory(kRx, aArgs); }
 
-template <> otError History::Process<Cmd("rxtx")>(Arg aArgs[])
-{
-    return ProcessRxTxHistory(kRxTx, aArgs);
-}
+template <> otError History::Process<Cmd("rxtx")>(Arg aArgs[]) { return ProcessRxTxHistory(kRxTx, aArgs); }
 
-template <> otError History::Process<Cmd("tx")>(Arg aArgs[])
-{
-    return ProcessRxTxHistory(kTx, aArgs);
-}
+template <> otError History::Process<Cmd("tx")>(Arg aArgs[]) { return ProcessRxTxHistory(kTx, aArgs); }
 
 const char *History::MessagePriorityToString(uint8_t aPriority)
 {
@@ -668,7 +745,7 @@
 
     static constexpr Command kCommands[] = {
         CmdEntry("ipaddr"), CmdEntry("ipmaddr"), CmdEntry("neighbor"), CmdEntry("netinfo"), CmdEntry("prefix"),
-        CmdEntry("route"),  CmdEntry("rx"),      CmdEntry("rxtx"),     CmdEntry("tx"),
+        CmdEntry("route"),  CmdEntry("router"),  CmdEntry("rx"),       CmdEntry("rxtx"),    CmdEntry("tx"),
     };
 
 #undef CmdEntry
diff --git a/src/cli/cli_history.hpp b/src/cli/cli_history.hpp
index a17a667..cab6d79 100644
--- a/src/cli/cli_history.hpp
+++ b/src/cli/cli_history.hpp
@@ -50,7 +50,7 @@
  * This class implements the History Tracker CLI interpreter.
  *
  */
-class History : private OutputWrapper
+class History : private Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg;
@@ -58,11 +58,12 @@
     /**
      * Constructor
      *
-     * @param[in]  aOutput The CLI console output context
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
      *
      */
-    explicit History(Output &aOutput)
-        : OutputWrapper(aOutput)
+    History(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+        : Output(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_joiner.cpp b/src/cli/cli_joiner.cpp
index 82d0d37..cf84968 100644
--- a/src/cli/cli_joiner.cpp
+++ b/src/cli/cli_joiner.cpp
@@ -62,7 +62,16 @@
 
         VerifyOrExit(discerner != nullptr, error = OT_ERROR_NOT_FOUND);
 
-        OutputLine("0x%llx/%u", static_cast<unsigned long long>(discerner->mValue), discerner->mLength);
+        if (discerner->mValue <= 0xffffffff)
+        {
+            OutputLine("0x%lx/%u", static_cast<unsigned long>(discerner->mValue & 0xffffffff), discerner->mLength);
+        }
+        else
+        {
+            OutputLine("0x%lx%08lx/%u", static_cast<unsigned long>(discerner->mValue >> 32),
+                       static_cast<unsigned long>(discerner->mValue & 0xffffffff), discerner->mLength);
+        }
+
         error = OT_ERROR_NONE;
     }
     else
@@ -252,10 +261,7 @@
     return error;
 }
 
-void Joiner::HandleCallback(otError aError, void *aContext)
-{
-    static_cast<Joiner *>(aContext)->HandleCallback(aError);
-}
+void Joiner::HandleCallback(otError aError, void *aContext) { static_cast<Joiner *>(aContext)->HandleCallback(aError); }
 
 void Joiner::HandleCallback(otError aError)
 {
diff --git a/src/cli/cli_joiner.hpp b/src/cli/cli_joiner.hpp
index d2bc640..cc3ddac 100644
--- a/src/cli/cli_joiner.hpp
+++ b/src/cli/cli_joiner.hpp
@@ -49,7 +49,7 @@
  * This class implements the Joiner CLI interpreter.
  *
  */
-class Joiner : private OutputWrapper
+class Joiner : private Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg;
@@ -57,11 +57,12 @@
     /**
      * Constructor
      *
-     * @param[in]  aOutput The CLI console output context
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
      *
      */
-    explicit Joiner(Output &aOutput)
-        : OutputWrapper(aOutput)
+    Joiner(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+        : Output(aInstance, aOutputImplementer)
     {
     }
 
diff --git a/src/cli/cli_network_data.cpp b/src/cli/cli_network_data.cpp
index 1c3de90..36fcf3d 100644
--- a/src/cli/cli_network_data.cpp
+++ b/src/cli/cli_network_data.cpp
@@ -146,7 +146,7 @@
 
 void NetworkData::OutputService(const otServiceConfig &aConfig)
 {
-    OutputFormat("%u ", aConfig.mEnterpriseNumber);
+    OutputFormat("%lu ", ToUlong(aConfig.mEnterpriseNumber));
     OutputBytes(aConfig.mServiceData, aConfig.mServiceDataLength);
     OutputFormat(" ");
     OutputBytes(aConfig.mServerConfig.mServerData, aConfig.mServerConfig.mServerDataLength);
@@ -159,6 +159,66 @@
     OutputLine(" %04x", aConfig.mServerConfig.mRloc16);
 }
 
+/**
+ * @cli netdata length
+ * @code
+ * netdata length
+ * 23
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otNetDataGetLength
+ */
+template <> otError NetworkData::Process<Cmd("length")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    OutputLine("%u", otNetDataGetLength(GetInstancePtr()));
+
+exit:
+    return error;
+}
+
+template <> otError NetworkData::Process<Cmd("maxlength")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    /**
+     * @cli netdata maxlength
+     * @code
+     * netdata maxlength
+     * 40
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otNetDataGetMaxLength
+     */
+    if (aArgs[0].IsEmpty())
+    {
+        OutputLine("%u", otNetDataGetMaxLength(GetInstancePtr()));
+    }
+    /**
+     * @cli netdata maxlength reset
+     * @code
+     * netdata maxlength reset
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otNetDataResetMaxLength
+     */
+    else if (aArgs[0] == "reset")
+    {
+        otNetDataResetMaxLength(GetInstancePtr());
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_ARGS;
+    }
+
+    return error;
+}
+
 #if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE
 template <> otError NetworkData::Process<Cmd("publish")>(Arg aArgs[])
 {
@@ -294,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;
@@ -546,6 +629,25 @@
     }
 }
 
+void NetworkData::OutputLowpanContexts(bool aLocal)
+{
+    otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
+    otLowpanContextInfo   info;
+
+    VerifyOrExit(!aLocal);
+
+    OutputLine("Contexts:");
+
+    while (otNetDataGetNextLowpanContextInfo(GetInstancePtr(), &iterator, &info) == OT_ERROR_NONE)
+    {
+        OutputIp6Prefix(info.mPrefix);
+        OutputLine(" %u %c", info.mContextId, info.mCompressFlag ? 'c' : '-');
+    }
+
+exit:
+    return;
+}
+
 otError NetworkData::OutputBinary(bool aLocal)
 {
     otError error;
@@ -583,6 +685,8 @@
  * Services:
  * 44970 5d c000 s 4000
  * 44970 01 9a04b000000e10 s 4000
+ * Contexts:
+ * fd00:dead:beef:cafe::/64 1 c
  * Done
  * @endcode
  * @code
@@ -595,7 +699,43 @@
  * @par
  * `netdata show` from OT CLI gets full Network Data received from the Leader. This command uses several
  * API functions to combine prefixes, routes, and services, including #otNetDataGetNextOnMeshPrefix,
- * #otNetDataGetNextRoute, and #otNetDataGetNextService.
+ * #otNetDataGetNextRoute, #otNetDataGetNextService and #otNetDataGetNextLowpanContextInfo.
+ * @par
+ * On-mesh prefixes are listed under `Prefixes` header:
+ * * The on-mesh prefix
+ * * Flags
+ *   * p: Preferred flag
+ *   * a: Stateless IPv6 Address Autoconfiguration flag
+ *   * d: DHCPv6 IPv6 Address Configuration flag
+ *   * c: DHCPv6 Other Configuration flag
+ *   * r: Default Route flag
+ *   * o: On Mesh flag
+ *   * s: Stable flag
+ *   * n: Nd Dns flag
+ *   * D: Domain Prefix flag (only available for Thread 1.2).
+ * * Preference `high`, `med`, or `low`
+ * * RLOC16 of device which added the on-mesh prefix
+ * @par
+ * External Routes are listed under `Routes` header:
+ * * The route prefix
+ * * Flags
+ *   * s: Stable flag
+ *   * n: NAT64 flag
+ * * Preference `high`, `med`, or `low`
+ * * RLOC16 of device which added the route prefix
+ * @par
+ * Service entries are listed under `Services` header:
+ * * Enterprise number
+ * * Service data (as hex bytes)
+ * * Server data (as hex bytes)
+ * * Flags
+ *   * s: Stable flag
+ * * RLOC16 of devices which added the service entry
+ * @par
+ * 6LoWPAN Context IDs are listed under `Contexts` header:
+ * * The prefix
+ * * Context ID
+ * * Compress flag (`c` if marked or `-` otherwise).
  * @par
  * @moreinfo{@netdata}.
  * @csa{br omrprefix}
@@ -654,6 +794,7 @@
         OutputPrefixes(local);
         OutputRoutes(local);
         OutputServices(local);
+        OutputLowpanContexts(local);
         error = OT_ERROR_NONE;
     }
 
@@ -669,6 +810,8 @@
     }
 
     static constexpr Command kCommands[] = {
+        CmdEntry("length"),
+        CmdEntry("maxlength"),
 #if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE
         CmdEntry("publish"),
 #endif
@@ -693,7 +836,8 @@
      * @cli netdata help
      * @code
      * netdata help
-     * help
+     * length
+     * maxlength
      * publish
      * register
      * show
diff --git a/src/cli/cli_network_data.hpp b/src/cli/cli_network_data.hpp
index d46ba9e..d751c45 100644
--- a/src/cli/cli_network_data.hpp
+++ b/src/cli/cli_network_data.hpp
@@ -47,7 +47,7 @@
  * This class implements the Network Data CLI.
  *
  */
-class NetworkData : private OutputWrapper
+class NetworkData : private Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg;
@@ -67,11 +67,12 @@
     /**
      * Constructor
      *
-     * @param[in]  aOutput The CLI console output context
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
      *
      */
-    explicit NetworkData(Output &aOutput)
-        : OutputWrapper(aOutput)
+    NetworkData(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+        : Output(aInstance, aOutputImplementer)
     {
     }
 
@@ -138,6 +139,7 @@
     void    OutputPrefixes(bool aLocal);
     void    OutputRoutes(bool aLocal);
     void    OutputServices(bool aLocal);
+    void    OutputLowpanContexts(bool aLocal);
 };
 
 } // namespace Cli
diff --git a/src/cli/cli_output.cpp b/src/cli/cli_output.cpp
index bd61c25..c70eacd 100644
--- a/src/cli/cli_output.cpp
+++ b/src/cli/cli_output.cpp
@@ -42,16 +42,16 @@
 #endif
 #include <openthread/logging.h>
 
+#include "cli/cli.hpp"
 #include "common/string.hpp"
 
 namespace ot {
 namespace Cli {
 
-const char OutputBase::kUnknownString[] = "unknown";
+const char Output::kUnknownString[] = "unknown";
 
-Output::Output(otInstance *aInstance, otCliOutputCallback aCallback, void *aCallbackContext)
-    : mInstance(aInstance)
-    , mCallback(aCallback)
+OutputImplementer::OutputImplementer(otCliOutputCallback aCallback, void *aCallbackContext)
+    : mCallback(aCallback)
     , mCallbackContext(aCallbackContext)
 #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
     , mOutputLength(0)
@@ -88,7 +88,7 @@
     OutputFormatV(aFormat, args);
     va_end(args);
 
-    OutputFormat("\r\n");
+    OutputNewLine();
 }
 
 void Output::OutputLine(uint8_t aIndentSize, const char *aFormat, ...)
@@ -101,17 +101,12 @@
     OutputFormatV(aFormat, args);
     va_end(args);
 
-    OutputFormat("\r\n");
+    OutputNewLine();
 }
 
-void Output::OutputSpaces(uint8_t aCount)
-{
-    char format[sizeof("%256s")];
+void Output::OutputNewLine(void) { OutputFormat("\r\n"); }
 
-    snprintf(format, sizeof(format), "%%%us", aCount);
-
-    OutputFormat(format, "");
-}
+void Output::OutputSpaces(uint8_t aCount) { OutputFormat("%*s", aCount, ""); }
 
 void Output::OutputBytes(const uint8_t *aBytes, uint16_t aLength)
 {
@@ -124,14 +119,45 @@
 void Output::OutputBytesLine(const uint8_t *aBytes, uint16_t aLength)
 {
     OutputBytes(aBytes, aLength);
-    OutputLine("");
+    OutputNewLine();
 }
 
-void Output::OutputEnabledDisabledStatus(bool aEnabled)
+const char *Output::Uint64ToString(uint64_t aUint64, Uint64StringBuffer &aBuffer)
 {
-    OutputLine(aEnabled ? "Enabled" : "Disabled");
+    char *cur = &aBuffer.mChars[Uint64StringBuffer::kSize - 1];
+
+    *cur = '\0';
+
+    if (aUint64 == 0)
+    {
+        *(--cur) = '0';
+    }
+    else
+    {
+        for (; aUint64 != 0; aUint64 /= 10)
+        {
+            *(--cur) = static_cast<char>('0' + static_cast<uint8_t>(aUint64 % 10));
+        }
+    }
+
+    return cur;
 }
 
+void Output::OutputUint64(uint64_t aUint64)
+{
+    Uint64StringBuffer buffer;
+
+    OutputFormat("%s", Uint64ToString(aUint64, buffer));
+}
+
+void Output::OutputUint64Line(uint64_t aUint64)
+{
+    OutputUint64(aUint64);
+    OutputNewLine();
+}
+
+void Output::OutputEnabledDisabledStatus(bool aEnabled) { OutputLine(aEnabled ? "Enabled" : "Disabled"); }
+
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
 
 void Output::OutputIp6Address(const otIp6Address &aAddress)
@@ -146,7 +172,7 @@
 void Output::OutputIp6AddressLine(const otIp6Address &aAddress)
 {
     OutputIp6Address(aAddress);
-    OutputLine("");
+    OutputNewLine();
 }
 
 void Output::OutputIp6Prefix(const otIp6Prefix &aPrefix)
@@ -161,7 +187,7 @@
 void Output::OutputIp6PrefixLine(const otIp6Prefix &aPrefix)
 {
     OutputIp6Prefix(aPrefix);
-    OutputLine("");
+    OutputNewLine();
 }
 
 void Output::OutputIp6Prefix(const otIp6NetworkPrefix &aPrefix)
@@ -173,7 +199,7 @@
 void Output::OutputIp6PrefixLine(const otIp6NetworkPrefix &aPrefix)
 {
     OutputIp6Prefix(aPrefix);
-    OutputLine("");
+    OutputNewLine();
 }
 
 void Output::OutputSockAddr(const otSockAddr &aSockAddr)
@@ -188,7 +214,7 @@
 void Output::OutputSockAddrLine(const otSockAddr &aSockAddr)
 {
     OutputSockAddr(aSockAddr);
-    OutputLine("");
+    OutputNewLine();
 }
 
 void Output::OutputDnsTxtData(const uint8_t *aTxtData, uint16_t aTxtDataLength)
@@ -235,9 +261,23 @@
 
     OutputFormat("]");
 }
+
+const char *Output::PercentageToString(uint16_t aValue, PercentageStringBuffer &aBuffer)
+{
+    uint32_t     scaledValue = aValue;
+    StringWriter writer(aBuffer.mChars, sizeof(aBuffer.mChars));
+
+    scaledValue = (scaledValue * 10000) / 0xffff;
+    writer.Append("%u.%02u", static_cast<uint16_t>(scaledValue / 100), static_cast<uint16_t>(scaledValue % 100));
+
+    return aBuffer.mChars;
+}
+
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
 
-void Output::OutputFormatV(const char *aFormat, va_list aArguments)
+void Output::OutputFormatV(const char *aFormat, va_list aArguments) { mImplementer.OutputV(aFormat, aArguments); }
+
+void OutputImplementer::OutputV(const char *aFormat, va_list aArguments)
 {
 #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
     va_list args;
@@ -279,7 +319,7 @@
 
         if (lineEnd > mOutputString)
         {
-            otLogCli(OT_LOG_LEVEL_DEBG, "Output: %s", mOutputString);
+            otLogCli(OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL, "Output: %s", mOutputString);
         }
 
         lineEnd++;
@@ -320,7 +360,7 @@
 
     if (truncated)
     {
-        otLogCli(OT_LOG_LEVEL_DEBG, "Output: %s ...", mOutputString);
+        otLogCli(OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL, "Output: %s ...", mOutputString);
         mOutputLength = 0;
     }
 
@@ -339,7 +379,7 @@
         inputString.Append(isFirst ? "%s" : " %s", aArgs->GetCString());
     }
 
-    otLogCli(OT_LOG_LEVEL_DEBG, "Input: %s", inputString.AsCString());
+    otLogCli(OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL, "Input: %s", inputString.AsCString());
 }
 #endif
 
diff --git a/src/cli/cli_output.hpp b/src/cli/cli_output.hpp
index 61e8997..1c46935 100644
--- a/src/cli/cli_output.hpp
+++ b/src/cli/cli_output.hpp
@@ -43,6 +43,7 @@
 #include "cli_config.h"
 
 #include "common/binary_search.hpp"
+#include "common/num_utils.hpp"
 #include "common/string.hpp"
 #include "utils/parse_cmdline.hpp"
 
@@ -68,11 +69,51 @@
     return (aString[0] == '\0') ? 0 : (static_cast<uint8_t>(aString[0]) + Cmd(aString + 1) * 255u);
 }
 
+class Output;
+
 /**
- * This class is the base class for `Output` and `OutputWrapper` providing common helper methods.
+ * This class implements the basic output functions.
  *
  */
-class OutputBase
+class OutputImplementer
+{
+    friend class Output;
+
+public:
+    /**
+     * This constructor initializes the `OutputImplementer` object.
+     *
+     * @param[in] aCallback           A pointer to an `otCliOutputCallback` to deliver strings to the CLI console.
+     * @param[in] aCallbackContext    An arbitrary context to pass in when invoking @p aCallback.
+     *
+     */
+    OutputImplementer(otCliOutputCallback aCallback, void *aCallbackContext);
+
+#if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
+    void SetEmittingCommandOutput(bool aEmittingOutput) { mEmittingCommandOutput = aEmittingOutput; }
+#else
+    void SetEmittingCommandOutput(bool) {}
+#endif
+
+private:
+    static constexpr uint16_t kInputOutputLogStringSize = OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LOG_STRING_SIZE;
+
+    void OutputV(const char *aFormat, va_list aArguments);
+
+    otCliOutputCallback mCallback;
+    void               *mCallbackContext;
+#if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
+    char     mOutputString[kInputOutputLogStringSize];
+    uint16_t mOutputLength;
+    bool     mEmittingCommandOutput;
+#endif
+};
+
+/**
+ * This class provides CLI output helper methods.
+ *
+ */
+class Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg; ///< An argument
@@ -141,26 +182,18 @@
         return (static_cast<uint16_t>(aEnum) < kLength) ? aTable[static_cast<uint16_t>(aEnum)] : aNotFound;
     }
 
-protected:
-    OutputBase(void) = default;
-};
-
-/**
- * This class provides CLI output helper methods.
- *
- */
-class Output : public OutputBase
-{
-public:
     /**
      * This constructor initializes the `Output` object.
      *
      * @param[in] aInstance           A pointer to OpenThread instance.
-     * @param[in] aCallback           A pointer to an `otCliOutputCallback` to deliver strings to the CLI console.
-     * @param[in] aCallbackContext    An arbitrary context to pass in when invoking @p aCallback.
+     * @param[in] aImplementer        An `OutputImplementer`.
      *
      */
-    Output(otInstance *aInstance, otCliOutputCallback aCallback, void *aCallbackContext);
+    Output(otInstance *aInstance, OutputImplementer &aImplementer)
+        : mInstance(aInstance)
+        , mImplementer(aImplementer)
+    {
+    }
 
     /**
      * This method returns the pointer to OpenThread instance.
@@ -171,13 +204,35 @@
     otInstance *GetInstancePtr(void) { return mInstance; }
 
     /**
+     * This structure represents a buffer which is used when converting a `uint64` value to string in decimal format.
+     *
+     */
+    struct Uint64StringBuffer
+    {
+        static constexpr uint16_t kSize = 21; ///< Size of a buffer
+
+        char mChars[kSize]; ///< Char array (do not access the array directly).
+    };
+
+    /**
+     * This static method converts a `uint64_t` value to a decimal format string.
+     *
+     * @param[in] aUint64  The `uint64_t` value to convert.
+     * @param[in] aBuffer  A buffer to allocate the string from.
+     *
+     * @returns A pointer to the start of the string (null-terminated) representation of @p aUint64.
+     *
+     */
+    static const char *Uint64ToString(uint64_t aUint64, Uint64StringBuffer &aBuffer);
+
+    /**
      * This method delivers a formatted output string to the CLI console.
      *
      * @param[in]  aFormat  A pointer to the format string.
      * @param[in]  ...      A variable list of arguments to format.
      *
      */
-    void OutputFormat(const char *aFormat, ...);
+    void OutputFormat(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
 
     /**
      * This method delivers a formatted output string to the CLI console (to which it prepends a given number
@@ -188,7 +243,7 @@
      * @param[in]  ...           A variable list of arguments to format.
      *
      */
-    void OutputFormat(uint8_t aIndentSize, const char *aFormat, ...);
+    void OutputFormat(uint8_t aIndentSize, const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(3, 4);
 
     /**
      * This method delivers a formatted output string to the CLI console (to which it also appends newline "\r\n").
@@ -197,7 +252,7 @@
      * @param[in]  ...      A variable list of arguments to format.
      *
      */
-    void OutputLine(const char *aFormat, ...);
+    void OutputLine(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
 
     /**
      * This method delivers a formatted output string to the CLI console (to which it prepends a given number
@@ -208,7 +263,13 @@
      * @param[in]  ...           A variable list of arguments to format.
      *
      */
-    void OutputLine(uint8_t aIndentSize, const char *aFormat, ...);
+    void OutputLine(uint8_t aIndentSize, const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(3, 4);
+
+    /**
+     * This method delivered newline "\r\n" to the CLI console.
+     *
+     */
+    void OutputNewLine(void);
 
     /**
      * This method outputs a given number of space chars to the CLI console.
@@ -281,6 +342,22 @@
     void OutputExtAddressLine(const otExtAddress &aExtAddress) { OutputBytesLine(aExtAddress.m8); }
 
     /**
+     * This method outputs a `uint64_t` value in decimal format.
+     *
+     * @param[in] aUint64   The `uint64_t` value to output.
+     *
+     */
+    void OutputUint64(uint64_t aUint64);
+
+    /**
+     * This method outputs a `uint64_t` value in decimal format and at the end it also outputs newline "\r\n".
+     *
+     * @param[in] aUint64   The `uint64_t` value to output.
+     *
+     */
+    void OutputUint64Line(uint64_t aUint64);
+
+    /**
      * This method outputs "Enabled" or "Disabled" status to the CLI console (it also appends newline "\r\n").
      *
      * @param[in] aEnabled  A boolean indicating the status. TRUE outputs "Enabled", FALSE outputs "Disabled".
@@ -363,6 +440,32 @@
      */
     void OutputDnsTxtData(const uint8_t *aTxtData, uint16_t aTxtDataLength);
 
+    /**
+     * This structure represents a buffer which is used when converting an encoded rate value to percentage string.
+     *
+     */
+    struct PercentageStringBuffer
+    {
+        static constexpr uint16_t kSize = 7; ///< Size of a buffer
+
+        char mChars[kSize]; ///< Char array (do not access the array directly).
+    };
+
+    /**
+     * This static method converts an encoded value to a percentage representation.
+     *
+     * The encoded @p aValue is assumed to be linearly scaled such that `0` maps to 0% and `0xffff` maps to 100%.
+     *
+     * The resulting string provides two decimal accuracy, e.g., "100.00", "0.00", "75.37".
+     *
+     * @param[in] aValue   The encoded percentage value to convert.
+     * @param[in] aBuffer  A buffer to allocate the string from.
+     *
+     * @returns A pointer to the start of the string (null-terminated) representation of @p aValue.
+     *
+     */
+    static const char *PercentageToString(uint16_t aValue, PercentageStringBuffer &aBuffer);
+
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
 
     /**
@@ -429,10 +532,8 @@
 
 #if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
     void LogInput(const Arg *aArgs);
-    void SetEmittingCommandOutput(bool aEmittingOutput) { mEmittingCommandOutput = aEmittingOutput; }
 #else
     void LogInput(const Arg *) {}
-    void SetEmittingCommandOutput(bool) {}
 #endif
 
 private:
@@ -441,96 +542,8 @@
     void OutputTableHeader(uint8_t aNumColumns, const char *const aTitles[], const uint8_t aWidths[]);
     void OutputTableSeparator(uint8_t aNumColumns, const uint8_t aWidths[]);
 
-    otInstance *        mInstance;
-    otCliOutputCallback mCallback;
-    void *              mCallbackContext;
-#if OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
-    char     mOutputString[kInputOutputLogStringSize];
-    uint16_t mOutputLength;
-    bool     mEmittingCommandOutput;
-#endif
-};
-
-class OutputWrapper : public OutputBase
-{
-protected:
-    explicit OutputWrapper(Output &aOutput)
-        : mOutput(aOutput)
-    {
-    }
-
-    otInstance *GetInstancePtr(void) { return mOutput.GetInstancePtr(); }
-
-    template <typename... Args> void OutputFormat(const char *aFormat, Args... aArgs)
-    {
-        mOutput.OutputFormat(aFormat, aArgs...);
-    }
-
-    template <typename... Args> void OutputFormat(uint8_t aIndentSize, const char *aFormat, Args... aArgs)
-    {
-        mOutput.OutputFormat(aIndentSize, aFormat, aArgs...);
-    }
-
-    template <typename... Args> void OutputLine(const char *aFormat, Args... aArgs)
-    {
-        return mOutput.OutputLine(aFormat, aArgs...);
-    }
-
-    template <typename... Args> void OutputLine(uint8_t aIndentSize, const char *aFormat, Args... aArgs)
-    {
-        return mOutput.OutputLine(aIndentSize, aFormat, aArgs...);
-    }
-
-    template <uint8_t kBytesLength> void OutputBytes(const uint8_t (&aBytes)[kBytesLength])
-    {
-        mOutput.OutputBytes(aBytes, kBytesLength);
-    }
-
-    template <uint8_t kBytesLength> void OutputBytesLine(const uint8_t (&aBytes)[kBytesLength])
-    {
-        mOutput.OutputBytesLine(aBytes, kBytesLength);
-    }
-
-    void OutputSpaces(uint8_t aCount) { return mOutput.OutputSpaces(aCount); }
-    void OutputBytes(const uint8_t *aBytes, uint16_t aLength) { return mOutput.OutputBytes(aBytes, aLength); }
-    void OutputBytesLine(const uint8_t *aBytes, uint16_t aLength) { return mOutput.OutputBytesLine(aBytes, aLength); }
-    void OutputExtAddress(const otExtAddress &aExtAddress) { mOutput.OutputExtAddress(aExtAddress); }
-    void OutputExtAddressLine(const otExtAddress &aExtAddress) { mOutput.OutputExtAddressLine(aExtAddress); }
-    void OutputEnabledDisabledStatus(bool aEnabled) { mOutput.OutputEnabledDisabledStatus(aEnabled); }
-
-#if OPENTHREAD_FTD || OPENTHREAD_MTD
-    void OutputIp6Address(const otIp6Address &aAddress) { mOutput.OutputIp6Address(aAddress); }
-    void OutputIp6AddressLine(const otIp6Address &aAddress) { mOutput.OutputIp6AddressLine(aAddress); }
-    void OutputIp6Prefix(const otIp6Prefix &aPrefix) { mOutput.OutputIp6Prefix(aPrefix); }
-    void OutputIp6PrefixLine(const otIp6Prefix &aPrefix) { mOutput.OutputIp6PrefixLine(aPrefix); }
-    void OutputIp6Prefix(const otIp6NetworkPrefix &aPrefix) { mOutput.OutputIp6Prefix(aPrefix); }
-    void OutputIp6PrefixLine(const otIp6NetworkPrefix &aPrefix) { mOutput.OutputIp6PrefixLine(aPrefix); }
-    void OutputSockAddr(const otSockAddr &aSockAddr) { mOutput.OutputSockAddr(aSockAddr); }
-    void OutputSockAddrLine(const otSockAddr &aSockAddr) { mOutput.OutputSockAddrLine(aSockAddr); }
-    void OutputDnsTxtData(const uint8_t *aTxtData, uint16_t aTxtDataLength)
-    {
-        mOutput.OutputDnsTxtData(aTxtData, aTxtDataLength);
-    }
-#endif
-
-    template <uint8_t kTableNumColumns>
-    void OutputTableHeader(const char *const (&aTitles)[kTableNumColumns], const uint8_t (&aWidths)[kTableNumColumns])
-    {
-        mOutput.OutputTableHeader(aTitles, aWidths);
-    }
-
-    template <uint8_t kTableNumColumns> void OutputTableSeparator(const uint8_t (&aWidths)[kTableNumColumns])
-    {
-        mOutput.OutputTableSeparator(aWidths);
-    }
-
-    template <typename Cli, uint16_t kLength> void OutputCommandTable(const CommandEntry<Cli> (&aCommandTable)[kLength])
-    {
-        mOutput.OutputCommandTable(aCommandTable);
-    }
-
-private:
-    Output &mOutput;
+    otInstance        *mInstance;
+    OutputImplementer &mImplementer;
 };
 
 } // namespace Cli
diff --git a/src/cli/cli_srp_client.cpp b/src/cli/cli_srp_client.cpp
index 0732ac2..0294c53 100644
--- a/src/cli/cli_srp_client.cpp
+++ b/src/cli/cli_srp_client.cpp
@@ -57,8 +57,8 @@
     return error;
 }
 
-SrpClient::SrpClient(Output &aOutput)
-    : OutputWrapper(aOutput)
+SrpClient::SrpClient(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+    : Output(aInstance, aOutputImplementer)
     , mCallbackEnabled(false)
 {
     otSrpClientSetCallback(GetInstancePtr(), SrpClient::HandleCallback, this);
@@ -129,7 +129,7 @@
         {
             uint16_t len;
             uint16_t size;
-            char *   hostName;
+            char    *hostName;
 
             VerifyOrExit(aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
             hostName = otSrpClientBuffersGetHostNameString(GetInstancePtr(), &size);
@@ -355,13 +355,13 @@
 
 otError SrpClient::ProcessServiceAdd(Arg aArgs[])
 {
-    // `add` <instance-name> <service-name> <port> [priority] [weight] [txt]
+    // `add` <instance-name> <service-name> <port> [priority] [weight] [txt] [lease] [key-lease]
 
     otSrpClientBuffersServiceEntry *entry = nullptr;
     uint16_t                        size;
-    char *                          string;
+    char                           *string;
     otError                         error;
-    char *                          label;
+    char                           *label;
 
     entry = otSrpClientBuffersAllocateService(GetInstancePtr());
 
@@ -417,7 +417,7 @@
         SuccessOrExit(error = aArgs[5].ParseAsUint16(entry->mService.mWeight));
     }
 
-    if (!aArgs[6].IsEmpty())
+    if (!aArgs[6].IsEmpty() && (aArgs[6] != "-"))
     {
         uint8_t *txtBuffer;
 
@@ -425,13 +425,23 @@
         entry->mTxtEntry.mValueLength = size;
 
         SuccessOrExit(error = aArgs[6].ParseAsHexString(entry->mTxtEntry.mValueLength, txtBuffer));
-        VerifyOrExit(aArgs[7].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
     }
     else
     {
         entry->mService.mNumTxtEntries = 0;
     }
 
+    if (!aArgs[7].IsEmpty())
+    {
+        SuccessOrExit(error = aArgs[7].ParseAsUint32(entry->mService.mLease));
+    }
+
+    if (!aArgs[8].IsEmpty())
+    {
+        SuccessOrExit(error = aArgs[8].ParseAsUint32(entry->mService.mKeyLease));
+        VerifyOrExit(aArgs[9].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    }
+
     SuccessOrExit(error = otSrpClientAddService(GetInstancePtr(), &entry->mService));
 
     entry = nullptr;
@@ -552,17 +562,17 @@
 
 void SrpClient::HandleCallback(otError                    aError,
                                const otSrpClientHostInfo *aHostInfo,
-                               const otSrpClientService * aServices,
-                               const otSrpClientService * aRemovedServices,
-                               void *                     aContext)
+                               const otSrpClientService  *aServices,
+                               const otSrpClientService  *aRemovedServices,
+                               void                      *aContext)
 {
     static_cast<SrpClient *>(aContext)->HandleCallback(aError, aHostInfo, aServices, aRemovedServices);
 }
 
 void SrpClient::HandleCallback(otError                    aError,
                                const otSrpClientHostInfo *aHostInfo,
-                               const otSrpClientService * aServices,
-                               const otSrpClientService * aRemovedServices)
+                               const otSrpClientService  *aServices,
+                               const otSrpClientService  *aRemovedServices)
 {
     otSrpClientService *next;
 
diff --git a/src/cli/cli_srp_client.hpp b/src/cli/cli_srp_client.hpp
index b868e6f..9b9ea42 100644
--- a/src/cli/cli_srp_client.hpp
+++ b/src/cli/cli_srp_client.hpp
@@ -51,7 +51,7 @@
  * This class implements the SRP Client CLI interpreter.
  *
  */
-class SrpClient : private OutputWrapper
+class SrpClient : private Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg;
@@ -59,10 +59,11 @@
     /**
      * Constructor
      *
-     * @param[in]  aOutput  The CLI console output context.
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
      *
      */
-    explicit SrpClient(Output &aOutput);
+    SrpClient(otInstance *aInstance, OutputImplementer &aOutputImplementer);
 
     /**
      * This method interprets a list of CLI arguments.
@@ -91,13 +92,13 @@
 
     static void HandleCallback(otError                    aError,
                                const otSrpClientHostInfo *aHostInfo,
-                               const otSrpClientService * aServices,
-                               const otSrpClientService * aRemovedServices,
-                               void *                     aContext);
+                               const otSrpClientService  *aServices,
+                               const otSrpClientService  *aRemovedServices,
+                               void                      *aContext);
     void        HandleCallback(otError                    aError,
                                const otSrpClientHostInfo *aHostInfo,
-                               const otSrpClientService * aServices,
-                               const otSrpClientService * aRemovedServices);
+                               const otSrpClientService  *aServices,
+                               const otSrpClientService  *aRemovedServices);
 
     bool mCallbackEnabled;
 };
diff --git a/src/cli/cli_srp_server.cpp b/src/cli/cli_srp_server.cpp
index e13b8ec..6a0a402 100644
--- a/src/cli/cli_srp_server.cpp
+++ b/src/cli/cli_srp_server.cpp
@@ -43,29 +43,7 @@
 namespace ot {
 namespace Cli {
 
-constexpr SrpServer::Command SrpServer::sCommands[];
-
-otError SrpServer::Process(Arg aArgs[])
-{
-    otError        error = OT_ERROR_INVALID_COMMAND;
-    const Command *command;
-
-    if (aArgs[0].IsEmpty())
-    {
-        IgnoreError(ProcessHelp(aArgs));
-        ExitNow();
-    }
-
-    command = BinarySearch::Find(aArgs[0].GetCString(), sCommands);
-    VerifyOrExit(command != nullptr);
-
-    error = (this->*command->mHandler)(aArgs + 1);
-
-exit:
-    return error;
-}
-
-otError SrpServer::ProcessAddrMode(Arg aArgs[])
+template <> otError SrpServer::Process<Cmd("addrmode")>(Arg aArgs[])
 {
     otError error = OT_ERROR_INVALID_ARGS;
 
@@ -96,7 +74,48 @@
     return error;
 }
 
-otError SrpServer::ProcessDomain(Arg aArgs[])
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+template <> otError SrpServer::Process<Cmd("auto")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    /**
+     * @cli srp server auto
+     * @code
+     * srp server auto
+     * Disabled
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otSrpServerIsAutoEnableMode
+     */
+    if (aArgs[0].IsEmpty())
+    {
+        OutputEnabledDisabledStatus(otSrpServerIsAutoEnableMode(GetInstancePtr()));
+    }
+    /**
+     * @cli srp server auto enable
+     * @code
+     * srp server auto enable
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otSrpServerSetAutoEnableMode
+     */
+    else
+    {
+        bool enable;
+
+        SuccessOrExit(error = Interpreter::ParseEnableOrDisable(aArgs[0], enable));
+        otSrpServerSetAutoEnableMode(GetInstancePtr(), enable);
+    }
+
+exit:
+    return error;
+}
+#endif
+
+template <> otError SrpServer::Process<Cmd("domain")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
 
@@ -112,7 +131,7 @@
     return error;
 }
 
-otError SrpServer::ProcessState(Arg aArgs[])
+template <> otError SrpServer::Process<Cmd("state")>(Arg aArgs[])
 {
     static const char *const kStateStrings[] = {
         "disabled", // (0) OT_SRP_SERVER_STATE_DISABLED
@@ -131,7 +150,7 @@
     return OT_ERROR_NONE;
 }
 
-otError SrpServer::ProcessEnable(Arg aArgs[])
+template <> otError SrpServer::Process<Cmd("enable")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
 
@@ -140,7 +159,7 @@
     return OT_ERROR_NONE;
 }
 
-otError SrpServer::ProcessDisable(Arg aArgs[])
+template <> otError SrpServer::Process<Cmd("disable")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
 
@@ -149,7 +168,7 @@
     return OT_ERROR_NONE;
 }
 
-otError SrpServer::ProcessTtl(Arg aArgs[])
+template <> otError SrpServer::Process<Cmd("ttl")>(Arg aArgs[])
 {
     otError              error = OT_ERROR_NONE;
     otSrpServerTtlConfig ttlConfig;
@@ -157,8 +176,8 @@
     if (aArgs[0].IsEmpty())
     {
         otSrpServerGetTtlConfig(GetInstancePtr(), &ttlConfig);
-        OutputLine("min ttl: %u", ttlConfig.mMinTtl);
-        OutputLine("max ttl: %u", ttlConfig.mMaxTtl);
+        OutputLine("min ttl: %lu", ToUlong(ttlConfig.mMinTtl));
+        OutputLine("max ttl: %lu", ToUlong(ttlConfig.mMaxTtl));
     }
     else
     {
@@ -173,7 +192,7 @@
     return error;
 }
 
-otError SrpServer::ProcessLease(Arg aArgs[])
+template <> otError SrpServer::Process<Cmd("lease")>(Arg aArgs[])
 {
     otError                error = OT_ERROR_NONE;
     otSrpServerLeaseConfig leaseConfig;
@@ -181,10 +200,10 @@
     if (aArgs[0].IsEmpty())
     {
         otSrpServerGetLeaseConfig(GetInstancePtr(), &leaseConfig);
-        OutputLine("min lease: %u", leaseConfig.mMinLease);
-        OutputLine("max lease: %u", leaseConfig.mMaxLease);
-        OutputLine("min key-lease: %u", leaseConfig.mMinKeyLease);
-        OutputLine("max key-lease: %u", leaseConfig.mMaxKeyLease);
+        OutputLine("min lease: %lu", ToUlong(leaseConfig.mMinLease));
+        OutputLine("max lease: %lu", ToUlong(leaseConfig.mMaxLease));
+        OutputLine("min key-lease: %lu", ToUlong(leaseConfig.mMinKeyLease));
+        OutputLine("max key-lease: %lu", ToUlong(leaseConfig.mMaxKeyLease));
     }
     else
     {
@@ -201,7 +220,7 @@
     return error;
 }
 
-otError SrpServer::ProcessHost(Arg aArgs[])
+template <> otError SrpServer::Process<Cmd("host")>(Arg aArgs[])
 {
     otError                error = OT_ERROR_NONE;
     const otSrpServerHost *host;
@@ -263,7 +282,7 @@
     OutputFormat("]");
 }
 
-otError SrpServer::ProcessService(Arg aArgs[])
+template <> otError SrpServer::Process<Cmd("service")>(Arg aArgs[])
 {
     static constexpr char *kAnyServiceName  = nullptr;
     static constexpr char *kAnyInstanceName = nullptr;
@@ -281,11 +300,12 @@
                                                          kAnyServiceName, kAnyInstanceName)) != nullptr)
         {
             bool                      isDeleted    = otSrpServerServiceIsDeleted(service);
-            const char *              instanceName = otSrpServerServiceGetInstanceName(service);
+            const char               *instanceName = otSrpServerServiceGetInstanceName(service);
             const otSrpServerService *subService   = nullptr;
-            const uint8_t *           txtData;
+            const uint8_t            *txtData;
             uint16_t                  txtDataLength;
             bool                      hasSubType = false;
+            otSrpServerLeaseInfo      leaseInfo;
 
             OutputLine("%s", instanceName);
             OutputLine(kIndentSize, "deleted: %s", isDeleted ? "true" : "false");
@@ -295,6 +315,8 @@
                 continue;
             }
 
+            otSrpServerServiceGetLeaseInfo(service, &leaseInfo);
+
             OutputFormat(kIndentSize, "subtypes: ");
 
             while ((subService = otSrpServerHostFindNextService(
@@ -310,21 +332,23 @@
 
             OutputLine(hasSubType ? "" : "(null)");
 
-            OutputLine(kIndentSize, "port: %hu", otSrpServerServiceGetPort(service));
-            OutputLine(kIndentSize, "priority: %hu", otSrpServerServiceGetPriority(service));
-            OutputLine(kIndentSize, "weight: %hu", otSrpServerServiceGetWeight(service));
-            OutputLine(kIndentSize, "ttl: %hu", otSrpServerServiceGetTtl(service));
+            OutputLine(kIndentSize, "port: %u", otSrpServerServiceGetPort(service));
+            OutputLine(kIndentSize, "priority: %u", otSrpServerServiceGetPriority(service));
+            OutputLine(kIndentSize, "weight: %u", otSrpServerServiceGetWeight(service));
+            OutputLine(kIndentSize, "ttl: %lu", ToUlong(otSrpServerServiceGetTtl(service)));
+            OutputLine(kIndentSize, "lease: %lu", ToUlong(leaseInfo.mLease / 1000));
+            OutputLine(kIndentSize, "key-lease: %lu", ToUlong(leaseInfo.mKeyLease / 1000));
 
             txtData = otSrpServerServiceGetTxtData(service, &txtDataLength);
             OutputFormat(kIndentSize, "TXT: ");
             OutputDnsTxtData(txtData, txtDataLength);
-            OutputLine("");
+            OutputNewLine();
 
             OutputLine(kIndentSize, "host: %s", otSrpServerHostGetFullName(host));
 
             OutputFormat(kIndentSize, "addresses: ");
             OutputHostAddresses(host);
-            OutputLine("");
+            OutputNewLine();
         }
     }
 
@@ -332,7 +356,7 @@
     return error;
 }
 
-otError SrpServer::ProcessSeqNum(Arg aArgs[])
+template <> otError SrpServer::Process<Cmd("seqnum")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
 
@@ -352,16 +376,47 @@
     return error;
 }
 
-otError SrpServer::ProcessHelp(Arg aArgs[])
+otError SrpServer::Process(Arg aArgs[])
 {
-    OT_UNUSED_VARIABLE(aArgs);
-
-    for (const Command &command : sCommands)
-    {
-        OutputLine(command.mName);
+#define CmdEntry(aCommandString)                                 \
+    {                                                            \
+        aCommandString, &SrpServer::Process<Cmd(aCommandString)> \
     }
 
-    return OT_ERROR_NONE;
+    static constexpr Command kCommands[] = {
+        CmdEntry("addrmode"),
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+        CmdEntry("auto"),
+#endif
+        CmdEntry("disable"),
+        CmdEntry("domain"),
+        CmdEntry("enable"),
+        CmdEntry("host"),
+        CmdEntry("lease"),
+        CmdEntry("seqnum"),
+        CmdEntry("service"),
+        CmdEntry("state"),
+        CmdEntry("ttl"),
+    };
+
+    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
diff --git a/src/cli/cli_srp_server.hpp b/src/cli/cli_srp_server.hpp
index 363f591..6d179d2 100644
--- a/src/cli/cli_srp_server.hpp
+++ b/src/cli/cli_srp_server.hpp
@@ -49,7 +49,7 @@
  * This class implements the SRP Server CLI interpreter.
  *
  */
-class SrpServer : private OutputWrapper
+class SrpServer : private Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg;
@@ -57,11 +57,12 @@
     /**
      * Constructor
      *
-     * @param[in]  aOutput  The CLI console output context.
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
      *
      */
-    explicit SrpServer(Output &aOutput)
-        : OutputWrapper(aOutput)
+    SrpServer(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+        : Output(aInstance, aOutputImplementer)
     {
     }
 
@@ -81,30 +82,9 @@
 
     using Command = CommandEntry<SrpServer>;
 
-    otError ProcessAddrMode(Arg aArgs[]);
-    otError ProcessDomain(Arg aArgs[]);
-    otError ProcessState(Arg aArgs[]);
-    otError ProcessEnable(Arg aArgs[]);
-    otError ProcessDisable(Arg aArgs[]);
-    otError ProcessLease(Arg aArgs[]);
-    otError ProcessHost(Arg aArgs[]);
-    otError ProcessService(Arg aArgs[]);
-    otError ProcessSeqNum(Arg aArgs[]);
-    otError ProcessTtl(Arg aArgs[]);
-    otError ProcessHelp(Arg aArgs[]);
+    template <CommandId kCommandId> otError Process(Arg aArgs[]);
 
     void OutputHostAddresses(const otSrpServerHost *aHost);
-
-    static constexpr Command sCommands[] = {
-        {"addrmode", &SrpServer::ProcessAddrMode}, {"disable", &SrpServer::ProcessDisable},
-        {"domain", &SrpServer::ProcessDomain},     {"enable", &SrpServer::ProcessEnable},
-        {"help", &SrpServer::ProcessHelp},         {"host", &SrpServer::ProcessHost},
-        {"lease", &SrpServer::ProcessLease},       {"seqnum", &SrpServer::ProcessSeqNum},
-        {"service", &SrpServer::ProcessService},   {"state", &SrpServer::ProcessState},
-        {"ttl", &SrpServer::ProcessTtl},
-    };
-
-    static_assert(BinarySearch::IsSorted(sCommands), "Command Table is not sorted");
 };
 
 } // namespace Cli
diff --git a/src/cli/cli_tcp.cpp b/src/cli/cli_tcp.cpp
index 97076e9..3e1cc82 100644
--- a/src/cli/cli_tcp.cpp
+++ b/src/cli/cli_tcp.cpp
@@ -39,40 +39,51 @@
 
 #include "cli_tcp.hpp"
 
+#include <openthread/nat64.h>
 #include <openthread/tcp.h>
 
 #include "cli/cli.hpp"
 #include "common/encoding.hpp"
 #include "common/timer.hpp"
 
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+#include <mbedtls/debug.h>
+#include <mbedtls/ecjpake.h>
+#include "crypto/mbedtls.hpp"
+#endif
+
 namespace ot {
 namespace Cli {
 
-constexpr TcpExample::Command TcpExample::sCommands[];
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+const int TcpExample::sCipherSuites[] = {MBEDTLS_TLS_ECJPAKE_WITH_AES_128_CCM_8,
+                                         MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8, 0};
+#endif
 
-TcpExample::TcpExample(Output &aOutput)
-    : OutputWrapper(aOutput)
+TcpExample::TcpExample(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+    : Output(aInstance, aOutputImplementer)
     , mInitialized(false)
     , mEndpointConnected(false)
     , mSendBusy(false)
+    , mUseCircularSendBuffer(true)
+    , mUseTls(false)
+    , mTlsHandshakeComplete(false)
     , mBenchmarkBytesTotal(0)
-    , mBenchmarkLinksLeft(0)
+    , mBenchmarkBytesUnsent(0)
 {
+    mEndpointAndCircularSendBuffer.mEndpoint   = &mEndpoint;
+    mEndpointAndCircularSendBuffer.mSendBuffer = &mSendBuffer;
 }
 
-otError TcpExample::ProcessHelp(Arg aArgs[])
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+void TcpExample::MbedTlsDebugOutput(void *ctx, int level, const char *file, int line, const char *str)
 {
-    OT_UNUSED_VARIABLE(aArgs);
-
-    for (const Command &command : sCommands)
-    {
-        OutputLine(command.mName);
-    }
-
-    return OT_ERROR_NONE;
+    TcpExample &tcpExample = *static_cast<TcpExample *>(ctx);
+    tcpExample.OutputLine("%s:%d:%d: %s", file, line, level, str);
 }
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
 
-otError TcpExample::ProcessInit(Arg aArgs[])
+template <> otError TcpExample::Process<Cmd("init")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
     size_t  receiveBufferSize;
@@ -81,30 +92,119 @@
 
     if (aArgs[0].IsEmpty())
     {
-        receiveBufferSize = sizeof(mReceiveBuffer);
+        mUseCircularSendBuffer = true;
+        mUseTls                = false;
+        receiveBufferSize      = sizeof(mReceiveBufferBytes);
     }
     else
     {
-        uint32_t windowSize;
+        if (aArgs[0] == "linked")
+        {
+            mUseCircularSendBuffer = false;
+            mUseTls                = false;
+        }
+        else if (aArgs[0] == "circular")
+        {
+            mUseCircularSendBuffer = true;
+            mUseTls                = false;
+        }
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+        else if (aArgs[0] == "tls")
+        {
+            mUseCircularSendBuffer = true;
+            mUseTls                = true;
 
-        SuccessOrExit(error = aArgs[0].ParseAsUint32(windowSize));
-        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+            // mbedtls_debug_set_threshold(0);
 
-        receiveBufferSize = windowSize + ((windowSize + 7) >> 3);
-        VerifyOrExit(receiveBufferSize <= sizeof(mReceiveBuffer) && receiveBufferSize != 0,
-                     error = OT_ERROR_INVALID_ARGS);
+            otPlatCryptoRandomInit();
+            mbedtls_x509_crt_init(&mSrvCert);
+            mbedtls_pk_init(&mPKey);
+
+            mbedtls_ssl_init(&mSslContext);
+            mbedtls_ssl_config_init(&mSslConfig);
+            mbedtls_ssl_conf_rng(&mSslConfig, Crypto::MbedTls::CryptoSecurePrng, nullptr);
+            // mbedtls_ssl_conf_dbg(&mSslConfig, MbedTlsDebugOutput, this);
+            mbedtls_ssl_conf_authmode(&mSslConfig, MBEDTLS_SSL_VERIFY_NONE);
+            mbedtls_ssl_conf_ciphersuites(&mSslConfig, sCipherSuites);
+
+#if (MBEDTLS_VERSION_NUMBER >= 0x03020000)
+            mbedtls_ssl_conf_min_tls_version(&mSslConfig, MBEDTLS_SSL_VERSION_TLS1_2);
+            mbedtls_ssl_conf_max_tls_version(&mSslConfig, MBEDTLS_SSL_VERSION_TLS1_2);
+#else
+            mbedtls_ssl_conf_min_version(&mSslConfig, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
+            mbedtls_ssl_conf_max_version(&mSslConfig, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
+#endif
+
+#if (MBEDTLS_VERSION_NUMBER >= 0x03000000)
+#include "crypto/mbedtls.hpp"
+            int rv = mbedtls_pk_parse_key(&mPKey, reinterpret_cast<const unsigned char *>(sSrvKey), sSrvKeyLength,
+                                          nullptr, 0, Crypto::MbedTls::CryptoSecurePrng, nullptr);
+#else
+            int rv = mbedtls_pk_parse_key(&mPKey, reinterpret_cast<const unsigned char *>(sSrvKey), sSrvKeyLength,
+                                          nullptr, 0);
+#endif
+            if (rv != 0)
+            {
+                OutputLine("mbedtls_pk_parse_key returned %d", rv);
+            }
+
+            rv = mbedtls_x509_crt_parse(&mSrvCert, reinterpret_cast<const unsigned char *>(sSrvPem), sSrvPemLength);
+            if (rv != 0)
+            {
+                OutputLine("mbedtls_x509_crt_parse (1) returned %d", rv);
+            }
+            rv = mbedtls_x509_crt_parse(&mSrvCert, reinterpret_cast<const unsigned char *>(sCasPem), sCasPemLength);
+            if (rv != 0)
+            {
+                OutputLine("mbedtls_x509_crt_parse (2) returned %d", rv);
+            }
+            rv = mbedtls_ssl_setup(&mSslContext, &mSslConfig);
+            if (rv != 0)
+            {
+                OutputLine("mbedtls_ssl_setup returned %d", rv);
+            }
+        }
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+        else
+        {
+            ExitNow(error = OT_ERROR_INVALID_ARGS);
+        }
+
+        if (aArgs[1].IsEmpty())
+        {
+            receiveBufferSize = sizeof(mReceiveBufferBytes);
+        }
+        else
+        {
+            uint32_t windowSize;
+
+            SuccessOrExit(error = aArgs[1].ParseAsUint32(windowSize));
+
+            receiveBufferSize = windowSize + ((windowSize + 7) >> 3);
+            VerifyOrExit(receiveBufferSize <= sizeof(mReceiveBufferBytes) && receiveBufferSize != 0,
+                         error = OT_ERROR_INVALID_ARGS);
+        }
     }
 
+    otTcpCircularSendBufferInitialize(&mSendBuffer, mSendBufferBytes, sizeof(mSendBufferBytes));
+
     {
         otTcpEndpointInitializeArgs endpointArgs;
 
         memset(&endpointArgs, 0x00, sizeof(endpointArgs));
-        endpointArgs.mEstablishedCallback      = HandleTcpEstablishedCallback;
-        endpointArgs.mSendDoneCallback         = HandleTcpSendDoneCallback;
+        endpointArgs.mEstablishedCallback = HandleTcpEstablishedCallback;
+        if (mUseCircularSendBuffer)
+        {
+            endpointArgs.mForwardProgressCallback = HandleTcpForwardProgressCallback;
+        }
+        else
+        {
+            endpointArgs.mSendDoneCallback = HandleTcpSendDoneCallback;
+        }
         endpointArgs.mReceiveAvailableCallback = HandleTcpReceiveAvailableCallback;
         endpointArgs.mDisconnectedCallback     = HandleTcpDisconnectedCallback;
         endpointArgs.mContext                  = this;
-        endpointArgs.mReceiveBuffer            = mReceiveBuffer;
+        endpointArgs.mReceiveBuffer            = mReceiveBufferBytes;
         endpointArgs.mReceiveBufferSize        = receiveBufferSize;
 
         SuccessOrExit(error = otTcpEndpointInitialize(GetInstancePtr(), &mEndpoint, &endpointArgs));
@@ -132,29 +232,46 @@
     return error;
 }
 
-otError TcpExample::ProcessDeinit(Arg aArgs[])
+template <> otError TcpExample::Process<Cmd("deinit")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
     otError endpointError;
+    otError bufferError;
     otError listenerError;
 
     VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
     VerifyOrExit(mInitialized, error = OT_ERROR_INVALID_STATE);
 
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    if (mUseTls)
+    {
+        otPlatCryptoRandomDeinit();
+        mbedtls_ssl_config_free(&mSslConfig);
+        mbedtls_ssl_free(&mSslContext);
+
+        mbedtls_pk_free(&mPKey);
+        mbedtls_x509_crt_free(&mSrvCert);
+    }
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+
     endpointError = otTcpEndpointDeinitialize(&mEndpoint);
     mSendBusy     = false;
 
+    otTcpCircularSendBufferForceDiscardAll(&mSendBuffer);
+    bufferError = otTcpCircularSendBufferDeinitialize(&mSendBuffer);
+
     listenerError = otTcpListenerDeinitialize(&mListener);
     mInitialized  = false;
 
     SuccessOrExit(error = endpointError);
+    SuccessOrExit(error = bufferError);
     SuccessOrExit(error = listenerError);
 
 exit:
     return error;
 }
 
-otError TcpExample::ProcessBind(Arg aArgs[])
+template <> otError TcpExample::Process<Cmd("bind")>(Arg aArgs[])
 {
     otError    error;
     otSockAddr sockaddr;
@@ -171,50 +288,94 @@
     return error;
 }
 
-otError TcpExample::ProcessConnect(Arg aArgs[])
+template <> otError TcpExample::Process<Cmd("connect")>(Arg aArgs[])
 {
     otError    error;
     otSockAddr sockaddr;
+    bool       nat64SynthesizedAddress;
 
     VerifyOrExit(mInitialized, error = OT_ERROR_INVALID_STATE);
 
-    SuccessOrExit(error = aArgs[0].ParseAsIp6Address(sockaddr.mAddress));
+    SuccessOrExit(
+        error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], sockaddr.mAddress, nat64SynthesizedAddress));
+    if (nat64SynthesizedAddress)
+    {
+        OutputFormat("Connecting to synthesized IPv6 address: ");
+        OutputIp6AddressLine(sockaddr.mAddress);
+    }
+
     SuccessOrExit(error = aArgs[1].ParseAsUint16(sockaddr.mPort));
     VerifyOrExit(aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
 
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    if (mUseTls)
+    {
+        int rv = mbedtls_ssl_config_defaults(&mSslConfig, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
+                                             MBEDTLS_SSL_PRESET_DEFAULT);
+        if (rv != 0)
+        {
+            OutputLine("mbedtls_ssl_config_defaults returned %d", rv);
+        }
+    }
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+
     SuccessOrExit(error = otTcpConnect(&mEndpoint, &sockaddr, OT_TCP_CONNECT_NO_FAST_OPEN));
-    mEndpointConnected = false;
+    mEndpointConnected = true;
 
 exit:
     return error;
 }
 
-otError TcpExample::ProcessSend(Arg aArgs[])
+template <> otError TcpExample::Process<Cmd("send")>(Arg aArgs[])
 {
     otError error;
 
     VerifyOrExit(mInitialized, error = OT_ERROR_INVALID_STATE);
-    VerifyOrExit(!mSendBusy, error = OT_ERROR_BUSY);
     VerifyOrExit(mBenchmarkBytesTotal == 0, error = OT_ERROR_BUSY);
-
-    mSendLink.mNext = nullptr;
-    mSendLink.mData = mSendBuffer;
     VerifyOrExit(!aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-    mSendLink.mLength = OT_MIN(aArgs[0].GetLength(), sizeof(mSendBuffer));
-    memcpy(mSendBuffer, aArgs[0].GetCString(), mSendLink.mLength);
     VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
 
-    SuccessOrExit(error = otTcpSendByReference(&mEndpoint, &mSendLink, 0));
-    mSendBusy = true;
+    if (mUseCircularSendBuffer)
+    {
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+        if (mUseTls)
+        {
+            int rv = mbedtls_ssl_write(&mSslContext, reinterpret_cast<unsigned char *>(aArgs[0].GetCString()),
+                                       aArgs[0].GetLength());
+            if (rv < 0 && rv != MBEDTLS_ERR_SSL_WANT_WRITE && rv != MBEDTLS_ERR_SSL_WANT_READ)
+            {
+                ExitNow(error = kErrorFailed);
+            }
+            error = kErrorNone;
+        }
+        else
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+        {
+            size_t written;
+            SuccessOrExit(error = otTcpCircularSendBufferWrite(&mEndpoint, &mSendBuffer, aArgs[0].GetCString(),
+                                                               aArgs[0].GetLength(), &written, 0));
+        }
+    }
+    else
+    {
+        VerifyOrExit(!mSendBusy, error = OT_ERROR_BUSY);
+
+        mSendLink.mNext   = nullptr;
+        mSendLink.mData   = mSendBufferBytes;
+        mSendLink.mLength = OT_MIN(aArgs[0].GetLength(), sizeof(mSendBufferBytes));
+        memcpy(mSendBufferBytes, aArgs[0].GetCString(), mSendLink.mLength);
+
+        SuccessOrExit(error = otTcpSendByReference(&mEndpoint, &mSendLink, 0));
+        mSendBusy = true;
+    }
 
 exit:
     return error;
 }
 
-otError TcpExample::ProcessBenchmark(Arg aArgs[])
+template <> otError TcpExample::Process<Cmd("benchmark")>(Arg aArgs[])
 {
-    otError  error = OT_ERROR_NONE;
-    uint32_t toSendOut;
+    otError error = OT_ERROR_NONE;
 
     VerifyOrExit(!mSendBusy, error = OT_ERROR_BUSY);
     VerifyOrExit(mBenchmarkBytesTotal == 0, error = OT_ERROR_BUSY);
@@ -230,34 +391,41 @@
     }
     VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
 
-    memset(mSendBuffer, 'a', sizeof(mSendBuffer));
+    mBenchmarkStart       = TimerMilli::GetNow();
+    mBenchmarkBytesUnsent = mBenchmarkBytesTotal;
 
-    mBenchmarkLinksLeft = (mBenchmarkBytesTotal + sizeof(mSendBuffer) - 1) / sizeof(mSendBuffer);
-    toSendOut           = OT_MIN(OT_ARRAY_LENGTH(mBenchmarkLinks), mBenchmarkLinksLeft);
-    mBenchmarkStart     = TimerMilli::GetNow();
-    for (uint32_t i = 0; i != toSendOut; i++)
+    if (mUseCircularSendBuffer)
     {
-        mBenchmarkLinks[i].mNext   = nullptr;
-        mBenchmarkLinks[i].mData   = mSendBuffer;
-        mBenchmarkLinks[i].mLength = sizeof(mSendBuffer);
-        if (i == 0 && mBenchmarkBytesTotal % sizeof(mSendBuffer) != 0)
+        SuccessOrExit(error = ContinueBenchmarkCircularSend());
+    }
+    else
+    {
+        uint32_t benchmarkLinksLeft = (mBenchmarkBytesTotal + sizeof(mSendBufferBytes) - 1) / sizeof(mSendBufferBytes);
+        uint32_t toSendOut          = OT_MIN(OT_ARRAY_LENGTH(mBenchmarkLinks), benchmarkLinksLeft);
+
+        /* We could also point the linked buffers directly to sBenchmarkData. */
+        memset(mSendBufferBytes, 'a', sizeof(mSendBufferBytes));
+
+        for (uint32_t i = 0; i != toSendOut; i++)
         {
-            mBenchmarkLinks[i].mLength = mBenchmarkBytesTotal % sizeof(mSendBuffer);
+            mBenchmarkLinks[i].mNext   = nullptr;
+            mBenchmarkLinks[i].mData   = mSendBufferBytes;
+            mBenchmarkLinks[i].mLength = sizeof(mSendBufferBytes);
+            if (i == 0 && mBenchmarkBytesTotal % sizeof(mSendBufferBytes) != 0)
+            {
+                mBenchmarkLinks[i].mLength = mBenchmarkBytesTotal % sizeof(mSendBufferBytes);
+            }
+            error = otTcpSendByReference(&mEndpoint, &mBenchmarkLinks[i],
+                                         i == toSendOut - 1 ? 0 : OT_TCP_SEND_MORE_TO_COME);
+            VerifyOrExit(error == OT_ERROR_NONE, mBenchmarkBytesTotal = 0);
         }
-        SuccessOrExit(error = otTcpSendByReference(&mEndpoint, &mBenchmarkLinks[i],
-                                                   i == toSendOut - 1 ? 0 : OT_TCP_SEND_MORE_TO_COME));
     }
 
 exit:
-    if (error != OT_ERROR_NONE)
-    {
-        mBenchmarkBytesTotal = 0;
-        mBenchmarkLinksLeft  = 0;
-    }
     return error;
 }
 
-otError TcpExample::ProcessSendEnd(Arg aArgs[])
+template <> otError TcpExample::Process<Cmd("sendend")>(Arg aArgs[])
 {
     otError error;
 
@@ -270,7 +438,7 @@
     return error;
 }
 
-otError TcpExample::ProcessAbort(Arg aArgs[])
+template <> otError TcpExample::Process<Cmd("abort")>(Arg aArgs[])
 {
     otError error;
 
@@ -284,7 +452,7 @@
     return error;
 }
 
-otError TcpExample::ProcessListen(Arg aArgs[])
+template <> otError TcpExample::Process<Cmd("listen")>(Arg aArgs[])
 {
     otError    error;
     otSockAddr sockaddr;
@@ -302,7 +470,7 @@
     return error;
 }
 
-otError TcpExample::ProcessStopListening(Arg aArgs[])
+template <> otError TcpExample::Process<Cmd("stoplistening")>(Arg aArgs[])
 {
     otError error;
 
@@ -317,13 +485,29 @@
 
 otError TcpExample::Process(Arg aArgs[])
 {
-    otError        error = OT_ERROR_INVALID_ARGS;
+#define CmdEntry(aCommandString)                                  \
+    {                                                             \
+        aCommandString, &TcpExample::Process<Cmd(aCommandString)> \
+    }
+
+    static constexpr Command kCommands[] = {
+        CmdEntry("abort"), CmdEntry("benchmark"), CmdEntry("bind"), CmdEntry("connect"), CmdEntry("deinit"),
+        CmdEntry("init"),  CmdEntry("listen"),    CmdEntry("send"), CmdEntry("sendend"), CmdEntry("stoplistening"),
+    };
+
+    static_assert(BinarySearch::IsSorted(kCommands), "kCommands is not sorted");
+
+    otError        error = OT_ERROR_INVALID_COMMAND;
     const Command *command;
 
-    VerifyOrExit(!aArgs[0].IsEmpty(), IgnoreError(ProcessHelp(nullptr)));
+    if (aArgs[0].IsEmpty() || (aArgs[0] == "help"))
+    {
+        OutputCommandTable(kCommands);
+        ExitNow(error = aArgs[0].IsEmpty() ? error : OT_ERROR_NONE);
+    }
 
-    command = BinarySearch::Find(aArgs[0].GetCString(), sCommands);
-    VerifyOrExit(command != nullptr, error = OT_ERROR_INVALID_COMMAND);
+    command = BinarySearch::Find(aArgs[0].GetCString(), kCommands);
+    VerifyOrExit(command != nullptr);
 
     error = (this->*command->mHandler)(aArgs + 1);
 
@@ -341,6 +525,12 @@
     static_cast<TcpExample *>(otTcpEndpointGetContext(aEndpoint))->HandleTcpSendDone(aEndpoint, aData);
 }
 
+void TcpExample::HandleTcpForwardProgressCallback(otTcpEndpoint *aEndpoint, size_t aInSendBuffer, size_t aBacklog)
+{
+    static_cast<TcpExample *>(otTcpEndpointGetContext(aEndpoint))
+        ->HandleTcpForwardProgress(aEndpoint, aInSendBuffer, aBacklog);
+}
+
 void TcpExample::HandleTcpReceiveAvailableCallback(otTcpEndpoint *aEndpoint,
                                                    size_t         aBytesAvailable,
                                                    bool           aEndOfStream,
@@ -355,16 +545,16 @@
     static_cast<TcpExample *>(otTcpEndpointGetContext(aEndpoint))->HandleTcpDisconnected(aEndpoint, aReason);
 }
 
-otTcpIncomingConnectionAction TcpExample::HandleTcpAcceptReadyCallback(otTcpListener *   aListener,
+otTcpIncomingConnectionAction TcpExample::HandleTcpAcceptReadyCallback(otTcpListener    *aListener,
                                                                        const otSockAddr *aPeer,
-                                                                       otTcpEndpoint **  aAcceptInto)
+                                                                       otTcpEndpoint   **aAcceptInto)
 {
     return static_cast<TcpExample *>(otTcpListenerGetContext(aListener))
         ->HandleTcpAcceptReady(aListener, aPeer, aAcceptInto);
 }
 
-void TcpExample::HandleTcpAcceptDoneCallback(otTcpListener *   aListener,
-                                             otTcpEndpoint *   aEndpoint,
+void TcpExample::HandleTcpAcceptDoneCallback(otTcpListener    *aListener,
+                                             otTcpEndpoint    *aEndpoint,
                                              const otSockAddr *aPeer)
 {
     static_cast<TcpExample *>(otTcpListenerGetContext(aListener))->HandleTcpAcceptDone(aListener, aEndpoint, aPeer);
@@ -374,11 +564,33 @@
 {
     OT_UNUSED_VARIABLE(aEndpoint);
     OutputLine("TCP: Connection established");
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    if (mUseTls)
+    {
+        int rv;
+        rv = mbedtls_ssl_set_hostname(&mSslContext, "localhost");
+        if (rv != 0)
+        {
+            OutputLine("mbedtls_ssl_set_hostname returned %d", rv);
+        }
+        rv = mbedtls_ssl_set_hs_ecjpake_password(
+            &mSslContext, reinterpret_cast<const unsigned char *>(sEcjpakePassword), sEcjpakePasswordLength);
+        if (rv != 0)
+        {
+            OutputLine("mbedtls_ssl_set_hs_ecjpake_password returned %d", rv);
+        }
+        mbedtls_ssl_set_bio(&mSslContext, &mEndpointAndCircularSendBuffer, otTcpMbedTlsSslSendCallback,
+                            otTcpMbedTlsSslRecvCallback, nullptr);
+        mTlsHandshakeComplete = false;
+        ContinueTLSHandshake();
+    }
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
 }
 
 void TcpExample::HandleTcpSendDone(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData)
 {
     OT_UNUSED_VARIABLE(aEndpoint);
+    OT_ASSERT(!mUseCircularSendBuffer); // this callback is not used when using the circular send buffer
 
     if (mBenchmarkBytesTotal == 0)
     {
@@ -393,25 +605,51 @@
     else
     {
         OT_ASSERT(aData != &mSendLink);
-        mBenchmarkLinksLeft--;
-        if (mBenchmarkLinksLeft >= OT_ARRAY_LENGTH(mBenchmarkLinks))
+        OT_ASSERT(mBenchmarkBytesUnsent >= aData->mLength);
+        mBenchmarkBytesUnsent -= aData->mLength; // could be less than sizeof(mSendBufferBytes) for the first link
+        if (mBenchmarkBytesUnsent >= OT_ARRAY_LENGTH(mBenchmarkLinks) * sizeof(mSendBufferBytes))
         {
-            aData->mLength = sizeof(mSendBuffer);
+            aData->mLength = sizeof(mSendBufferBytes);
             if (otTcpSendByReference(&mEndpoint, aData, 0) != OT_ERROR_NONE)
             {
                 OutputLine("TCP Benchmark Failed");
                 mBenchmarkBytesTotal = 0;
             }
         }
-        else if (mBenchmarkLinksLeft == 0)
+        else if (mBenchmarkBytesUnsent == 0)
         {
-            uint32_t milliseconds         = TimerMilli::GetNow() - mBenchmarkStart;
-            uint32_t thousandTimesGoodput = (1000 * (mBenchmarkBytesTotal << 3) + (milliseconds >> 1)) / milliseconds;
+            CompleteBenchmark();
+        }
+    }
+}
 
-            OutputLine("TCP Benchmark Complete: Transferred %u bytes in %u milliseconds",
-                       static_cast<unsigned int>(mBenchmarkBytesTotal), static_cast<unsigned int>(milliseconds));
-            OutputLine("TCP Goodput: %u.%03u kb/s", thousandTimesGoodput / 1000, thousandTimesGoodput % 1000);
-            mBenchmarkBytesTotal = 0;
+void TcpExample::HandleTcpForwardProgress(otTcpEndpoint *aEndpoint, size_t aInSendBuffer, size_t aBacklog)
+{
+    OT_UNUSED_VARIABLE(aEndpoint);
+    OT_UNUSED_VARIABLE(aBacklog);
+    OT_ASSERT(mUseCircularSendBuffer); // this callback is only used when using the circular send buffer
+
+    otTcpCircularSendBufferHandleForwardProgress(&mSendBuffer, aInSendBuffer);
+
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    if (mUseTls)
+    {
+        ContinueTLSHandshake();
+    }
+#endif
+
+    /* Handle case where we're in a benchmark. */
+    if (mBenchmarkBytesTotal != 0)
+    {
+        if (mBenchmarkBytesUnsent != 0)
+        {
+            /* Continue sending out data if there's data we haven't sent. */
+            IgnoreError(ContinueBenchmarkCircularSend());
+        }
+        else if (aInSendBuffer == 0)
+        {
+            /* Handle case where all data is sent out and the send buffer has drained. */
+            CompleteBenchmark();
         }
     }
 }
@@ -424,20 +662,53 @@
     OT_UNUSED_VARIABLE(aBytesRemaining);
     OT_ASSERT(aEndpoint == &mEndpoint);
 
-    if (aBytesAvailable > 0)
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    if (mUseTls && ContinueTLSHandshake())
     {
-        const otLinkedBuffer *data;
-        size_t                totalReceived = 0;
+        return;
+    }
+#endif
 
-        IgnoreError(otTcpReceiveByReference(aEndpoint, &data));
-        for (; data != nullptr; data = data->mNext)
+    if ((mTlsHandshakeComplete || !mUseTls) && aBytesAvailable > 0)
+    {
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+        if (mUseTls)
         {
-            OutputLine("TCP: Received %u bytes: %.*s", static_cast<unsigned int>(data->mLength), data->mLength,
-                       reinterpret_cast<const char *>(data->mData));
-            totalReceived += data->mLength;
+            uint8_t buffer[500];
+            for (;;)
+            {
+                int rv = mbedtls_ssl_read(&mSslContext, buffer, sizeof(buffer));
+                if (rv < 0)
+                {
+                    if (rv == MBEDTLS_ERR_SSL_WANT_READ)
+                    {
+                        break;
+                    }
+                    OutputLine("TLS receive failure: %d", rv);
+                }
+                else
+                {
+                    OutputLine("TLS: Received %u bytes: %.*s", static_cast<unsigned>(rv), rv,
+                               reinterpret_cast<const char *>(buffer));
+                }
+            }
+            OutputLine("(TCP: Received %u bytes)", static_cast<unsigned>(aBytesAvailable));
         }
-        OT_ASSERT(aBytesAvailable == totalReceived);
-        IgnoreReturnValue(otTcpCommitReceive(aEndpoint, totalReceived, 0));
+        else
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+        {
+            const otLinkedBuffer *data;
+            size_t                totalReceived = 0;
+            IgnoreError(otTcpReceiveByReference(aEndpoint, &data));
+            for (; data != nullptr; data = data->mNext)
+            {
+                OutputLine("TCP: Received %u bytes: %.*s", static_cast<unsigned>(data->mLength),
+                           static_cast<unsigned>(data->mLength), reinterpret_cast<const char *>(data->mData));
+                totalReceived += data->mLength;
+            }
+            OT_ASSERT(aBytesAvailable == totalReceived);
+            IgnoreReturnValue(otTcpCommitReceive(aEndpoint, totalReceived, 0));
+        }
     }
 
     if (aEndOfStream)
@@ -466,6 +737,13 @@
 
     OutputLine("TCP: %s", Stringify(aReason, kReasonStrings));
 
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    if (mUseTls)
+    {
+        mbedtls_ssl_session_reset(&mSslContext);
+    }
+#endif
+
     // We set this to false even for the TIME-WAIT state, so that we can reuse
     // the active socket if an incoming connection comes in instead of waiting
     // for the 2MSL timeout.
@@ -473,17 +751,18 @@
     mSendBusy          = false;
 
     // Mark the benchmark as inactive if the connection was disconnected.
-    if (mBenchmarkBytesTotal != 0)
-    {
-        mBenchmarkBytesTotal = 0;
-        mBenchmarkLinksLeft  = 0;
-    }
+    mBenchmarkBytesTotal  = 0;
+    mBenchmarkBytesUnsent = 0;
+
+    otTcpCircularSendBufferForceDiscardAll(&mSendBuffer);
 }
 
-otTcpIncomingConnectionAction TcpExample::HandleTcpAcceptReady(otTcpListener *   aListener,
+otTcpIncomingConnectionAction TcpExample::HandleTcpAcceptReady(otTcpListener    *aListener,
                                                                const otSockAddr *aPeer,
-                                                               otTcpEndpoint **  aAcceptInto)
+                                                               otTcpEndpoint   **aAcceptInto)
 {
+    otTcpIncomingConnectionAction action;
+
     OT_UNUSED_VARIABLE(aListener);
 
     if (mEndpointConnected)
@@ -492,11 +771,14 @@
         OutputSockAddr(*aPeer);
         OutputLine(" (active socket is busy)");
 
-        return OT_TCP_INCOMING_CONNECTION_ACTION_DEFER;
+        ExitNow(action = OT_TCP_INCOMING_CONNECTION_ACTION_DEFER);
     }
 
     *aAcceptInto = &mEndpoint;
-    return OT_TCP_INCOMING_CONNECTION_ACTION_ACCEPT;
+    action       = OT_TCP_INCOMING_CONNECTION_ACTION_ACCEPT;
+
+exit:
+    return action;
 }
 
 void TcpExample::HandleTcpAcceptDone(otTcpListener *aListener, otTcpEndpoint *aEndpoint, const otSockAddr *aPeer)
@@ -504,10 +786,117 @@
     OT_UNUSED_VARIABLE(aListener);
     OT_UNUSED_VARIABLE(aEndpoint);
 
+    mEndpointConnected = true;
     OutputFormat("Accepted connection from ");
     OutputSockAddrLine(*aPeer);
+
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    if (mUseTls)
+    {
+        int rv;
+
+        rv = mbedtls_ssl_config_defaults(&mSslConfig, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM,
+                                         MBEDTLS_SSL_PRESET_DEFAULT);
+        if (rv != 0)
+        {
+            OutputLine("mbedtls_ssl_config_defaults returned %d", rv);
+        }
+        mbedtls_ssl_conf_ca_chain(&mSslConfig, mSrvCert.next, nullptr);
+        rv = mbedtls_ssl_conf_own_cert(&mSslConfig, &mSrvCert, &mPKey);
+        if (rv != 0)
+        {
+            OutputLine("mbedtls_ssl_conf_own_cert returned %d", rv);
+        }
+    }
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
 }
 
+otError TcpExample::ContinueBenchmarkCircularSend(void)
+{
+    otError error = OT_ERROR_NONE;
+    size_t  freeSpace;
+
+    while (mBenchmarkBytesUnsent != 0 && (freeSpace = otTcpCircularSendBufferGetFreeSpace(&mSendBuffer)) != 0)
+    {
+        size_t   toSendThisIteration = OT_MIN(mBenchmarkBytesUnsent, sBenchmarkDataLength);
+        uint32_t flag                = (toSendThisIteration < freeSpace && toSendThisIteration < mBenchmarkBytesUnsent)
+                                           ? OT_TCP_CIRCULAR_SEND_BUFFER_WRITE_MORE_TO_COME
+                                           : 0;
+        size_t   written             = 0;
+
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+        if (mUseTls)
+        {
+            int rv = mbedtls_ssl_write(&mSslContext, reinterpret_cast<const unsigned char *>(sBenchmarkData),
+                                       toSendThisIteration);
+            if (rv > 0)
+            {
+                written = static_cast<size_t>(rv);
+                OT_ASSERT(written <= mBenchmarkBytesUnsent);
+            }
+            else if (rv != MBEDTLS_ERR_SSL_WANT_WRITE && rv != MBEDTLS_ERR_SSL_WANT_READ)
+            {
+                ExitNow(error = kErrorFailed);
+            }
+            error = kErrorNone;
+        }
+        else
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+        {
+            SuccessOrExit(error = otTcpCircularSendBufferWrite(&mEndpoint, &mSendBuffer, sBenchmarkData,
+                                                               toSendThisIteration, &written, flag));
+        }
+        mBenchmarkBytesUnsent -= written;
+    }
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        OutputLine("TCP Benchmark Failed");
+        mBenchmarkBytesTotal  = 0;
+        mBenchmarkBytesUnsent = 0;
+    }
+
+    return error;
+}
+
+void TcpExample::CompleteBenchmark(void)
+{
+    uint32_t milliseconds         = TimerMilli::GetNow() - mBenchmarkStart;
+    uint32_t thousandTimesGoodput = (1000 * (mBenchmarkBytesTotal << 3) + (milliseconds >> 1)) / milliseconds;
+
+    OutputLine("TCP Benchmark Complete: Transferred %lu bytes in %lu milliseconds", ToUlong(mBenchmarkBytesTotal),
+               ToUlong(milliseconds));
+    OutputLine("TCP Goodput: %lu.%03u kb/s", ToUlong(thousandTimesGoodput / 1000),
+               static_cast<uint16_t>(thousandTimesGoodput % 1000));
+    mBenchmarkBytesTotal = 0;
+}
+
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+bool TcpExample::ContinueTLSHandshake(void)
+{
+    bool wasNotAlreadyDone = false;
+    int  rv;
+
+    if (!mTlsHandshakeComplete)
+    {
+        rv = mbedtls_ssl_handshake(&mSslContext);
+        if (rv == 0)
+        {
+            OutputLine("TLS Handshake Complete");
+            mTlsHandshakeComplete = true;
+        }
+        else if (rv != MBEDTLS_ERR_SSL_WANT_READ && rv != MBEDTLS_ERR_SSL_WANT_WRITE)
+        {
+            OutputLine("TLS Handshake Failed: %d", rv);
+        }
+        wasNotAlreadyDone = true;
+    }
+
+    return wasNotAlreadyDone;
+}
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+
 } // namespace Cli
 } // namespace ot
 
diff --git a/src/cli/cli_tcp.hpp b/src/cli/cli_tcp.hpp
index 4bebc96..9af459d 100644
--- a/src/cli/cli_tcp.hpp
+++ b/src/cli/cli_tcp.hpp
@@ -37,6 +37,16 @@
 #include "openthread-core-config.h"
 
 #include <openthread/tcp.h>
+#include <openthread/tcp_ext.h>
+
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+
+#include <mbedtls/ctr_drbg.h>
+#include <mbedtls/entropy.h>
+#include <mbedtls/ssl.h>
+#include <mbedtls/x509_crt.h>
+
+#endif
 
 #include "cli/cli_config.h"
 #include "cli/cli_output.hpp"
@@ -49,7 +59,7 @@
  * This class implements a CLI-based TCP example.
  *
  */
-class TcpExample : private OutputWrapper
+class TcpExample : private Output
 {
 public:
     using Arg = Utils::CmdLineParser::Arg;
@@ -57,10 +67,11 @@
     /**
      * Constructor
      *
-     * @param[in]  aOutput  The CLI console output context.
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
      *
      */
-    explicit TcpExample(Output &aOutput);
+    TcpExample(otInstance *aInstance, OutputImplementer &aOutputImplementer);
 
     /**
      * This method interprets a list of CLI arguments.
@@ -73,59 +84,46 @@
 private:
     using Command = CommandEntry<TcpExample>;
 
-    otError ProcessHelp(Arg aArgs[]);
-    otError ProcessInit(Arg aArgs[]);
-    otError ProcessDeinit(Arg aArgs[]);
-    otError ProcessBind(Arg aArgs[]);
-    otError ProcessConnect(Arg aArgs[]);
-    otError ProcessSend(Arg aArgs[]);
-    otError ProcessBenchmark(Arg aArgs[]);
-    otError ProcessSendEnd(Arg aArgs[]);
-    otError ProcessAbort(Arg aArgs[]);
-    otError ProcessListen(Arg aArgs[]);
-    otError ProcessStopListening(Arg aArgs[]);
+    template <CommandId kCommandId> otError Process(Arg aArgs[]);
+
+    otError ContinueBenchmarkCircularSend(void);
+    void    CompleteBenchmark(void);
+
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    bool ContinueTLSHandshake(void);
+#endif
 
     static void HandleTcpEstablishedCallback(otTcpEndpoint *aEndpoint);
     static void HandleTcpSendDoneCallback(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData);
+    static void HandleTcpForwardProgressCallback(otTcpEndpoint *aEndpoint, size_t aInSendBuffer, size_t aBacklog);
     static void HandleTcpReceiveAvailableCallback(otTcpEndpoint *aEndpoint,
                                                   size_t         aBytesAvailable,
                                                   bool           aEndOfStream,
                                                   size_t         aBytesRemaining);
     static void HandleTcpDisconnectedCallback(otTcpEndpoint *aEndpoint, otTcpDisconnectedReason aReason);
-    static otTcpIncomingConnectionAction HandleTcpAcceptReadyCallback(otTcpListener *   aListener,
+    static otTcpIncomingConnectionAction HandleTcpAcceptReadyCallback(otTcpListener    *aListener,
                                                                       const otSockAddr *aPeer,
-                                                                      otTcpEndpoint **  aAcceptInto);
-    static void                          HandleTcpAcceptDoneCallback(otTcpListener *   aListener,
-                                                                     otTcpEndpoint *   aEndpoint,
+                                                                      otTcpEndpoint   **aAcceptInto);
+    static void                          HandleTcpAcceptDoneCallback(otTcpListener    *aListener,
+                                                                     otTcpEndpoint    *aEndpoint,
                                                                      const otSockAddr *aPeer);
 
-    void                          HandleTcpEstablished(otTcpEndpoint *aEndpoint);
-    void                          HandleTcpSendDone(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData);
-    void                          HandleTcpReceiveAvailable(otTcpEndpoint *aEndpoint,
-                                                            size_t         aBytesAvailable,
-                                                            bool           aEndOfStream,
-                                                            size_t         aBytesRemaining);
-    void                          HandleTcpDisconnected(otTcpEndpoint *aEndpoint, otTcpDisconnectedReason aReason);
-    otTcpIncomingConnectionAction HandleTcpAcceptReady(otTcpListener *   aListener,
+    void HandleTcpEstablished(otTcpEndpoint *aEndpoint);
+    void HandleTcpSendDone(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData);
+    void HandleTcpForwardProgress(otTcpEndpoint *aEndpoint, size_t aInSendBuffer, size_t aBacklog);
+    void HandleTcpReceiveAvailable(otTcpEndpoint *aEndpoint,
+                                   size_t         aBytesAvailable,
+                                   bool           aEndOfStream,
+                                   size_t         aBytesRemaining);
+    void HandleTcpDisconnected(otTcpEndpoint *aEndpoint, otTcpDisconnectedReason aReason);
+    otTcpIncomingConnectionAction HandleTcpAcceptReady(otTcpListener    *aListener,
                                                        const otSockAddr *aPeer,
-                                                       otTcpEndpoint **  aAcceptInto);
+                                                       otTcpEndpoint   **aAcceptInto);
     void HandleTcpAcceptDone(otTcpListener *aListener, otTcpEndpoint *aEndpoint, const otSockAddr *aPeer);
 
-    static constexpr Command sCommands[] = {
-        {"abort", &TcpExample::ProcessAbort},
-        {"benchmark", &TcpExample::ProcessBenchmark},
-        {"bind", &TcpExample::ProcessBind},
-        {"connect", &TcpExample::ProcessConnect},
-        {"deinit", &TcpExample::ProcessDeinit},
-        {"help", &TcpExample::ProcessHelp},
-        {"init", &TcpExample::ProcessInit},
-        {"listen", &TcpExample::ProcessListen},
-        {"send", &TcpExample::ProcessSend},
-        {"sendend", &TcpExample::ProcessSendEnd},
-        {"stoplistening", &TcpExample::ProcessStopListening},
-    };
-
-    static_assert(BinarySearch::IsSorted(sCommands), "Command Table is not sorted");
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    static void MbedTlsDebugOutput(void *ctx, int level, const char *file, int line, const char *str);
+#endif
 
     otTcpEndpoint mEndpoint;
     otTcpListener mListener;
@@ -133,15 +131,86 @@
     bool mInitialized;
     bool mEndpointConnected;
     bool mSendBusy;
+    bool mUseCircularSendBuffer;
+    bool mUseTls;
+    bool mTlsHandshakeComplete;
 
-    otLinkedBuffer mSendLink;
-    uint8_t        mSendBuffer[OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH];
-    uint8_t        mReceiveBuffer[OPENTHREAD_CONFIG_CLI_TCP_RECEIVE_BUFFER_SIZE];
+    otTcpCircularSendBuffer mSendBuffer;
+    otLinkedBuffer          mSendLink;
+    uint8_t                 mSendBufferBytes[OPENTHREAD_CONFIG_CLI_TCP_RECEIVE_BUFFER_SIZE];
+    uint8_t                 mReceiveBufferBytes[OPENTHREAD_CONFIG_CLI_TCP_RECEIVE_BUFFER_SIZE];
 
-    otLinkedBuffer mBenchmarkLinks[(sizeof(mReceiveBuffer) + sizeof(mSendBuffer) - 1) / sizeof(mSendBuffer)];
-    uint32_t       mBenchmarkBytesTotal;
-    uint32_t       mBenchmarkLinksLeft;
-    TimeMilli      mBenchmarkStart;
+    otLinkedBuffer
+              mBenchmarkLinks[(sizeof(mReceiveBufferBytes) + sizeof(mSendBufferBytes) - 1) / sizeof(mSendBufferBytes)];
+    uint32_t  mBenchmarkBytesTotal;
+    uint32_t  mBenchmarkBytesUnsent;
+    TimeMilli mBenchmarkStart;
+
+    otTcpEndpointAndCircularSendBuffer mEndpointAndCircularSendBuffer;
+
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    mbedtls_ssl_context     mSslContext;
+    mbedtls_ssl_config      mSslConfig;
+    mbedtls_x509_crt        mSrvCert;
+    mbedtls_pk_context      mPKey;
+    mbedtls_entropy_context mEntropy;
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+
+    static constexpr const char *sBenchmarkData =
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+    static constexpr const size_t sBenchmarkDataLength = 1040;
+
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+    static constexpr const char  *sCasPem       = "-----BEGIN CERTIFICATE-----\r\n"
+                                                  "MIIBtDCCATqgAwIBAgIBTTAKBggqhkjOPQQDAjBLMQswCQYDVQQGEwJOTDERMA8G\r\n"
+                                                  "A1UEChMIUG9sYXJTU0wxKTAnBgNVBAMTIFBvbGFyU1NMIFRlc3QgSW50ZXJtZWRp\r\n"
+                                                  "YXRlIEVDIENBMB4XDTE1MDkwMTE0MDg0M1oXDTI1MDgyOTE0MDg0M1owSjELMAkG\r\n"
+                                                  "A1UEBhMCVUsxETAPBgNVBAoTCG1iZWQgVExTMSgwJgYDVQQDEx9tYmVkIFRMUyBU\r\n"
+                                                  "ZXN0IGludGVybWVkaWF0ZSBDQSAzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\r\n"
+                                                  "732fWHLNPMPsP1U1ibXvb55erlEVMlpXBGsj+KYwVqU1XCmW9Z9hhP7X/5js/DX9\r\n"
+                                                  "2J/utoHyjUtVpQOzdTrbsaMQMA4wDAYDVR0TBAUwAwEB/zAKBggqhkjOPQQDAgNo\r\n"
+                                                  "ADBlAjAJRxbGRas3NBmk9MnGWXg7PT1xnRELHRWWIvfLdVQt06l1/xFg3ZuPdQdt\r\n"
+                                                  "Qh7CK80CMQD7wa1o1a8qyDKBfLN636uKmKGga0E+vYXBeFCy9oARBangGCB0B2vt\r\n"
+                                                  "pz590JvGWfM=\r\n"
+                                                  "-----END CERTIFICATE-----\r\n";
+    static constexpr const size_t sCasPemLength = 665; // includes NUL byte
+
+    static constexpr const char  *sSrvPem       = "-----BEGIN CERTIFICATE-----\r\n"
+                                                  "MIICHzCCAaWgAwIBAgIBCTAKBggqhkjOPQQDAjA+MQswCQYDVQQGEwJOTDERMA8G\r\n"
+                                                  "A1UEChMIUG9sYXJTU0wxHDAaBgNVBAMTE1BvbGFyc3NsIFRlc3QgRUMgQ0EwHhcN\r\n"
+                                                  "MTMwOTI0MTU1MjA0WhcNMjMwOTIyMTU1MjA0WjA0MQswCQYDVQQGEwJOTDERMA8G\r\n"
+                                                  "A1UEChMIUG9sYXJTU0wxEjAQBgNVBAMTCWxvY2FsaG9zdDBZMBMGByqGSM49AgEG\r\n"
+                                                  "CCqGSM49AwEHA0IABDfMVtl2CR5acj7HWS3/IG7ufPkGkXTQrRS192giWWKSTuUA\r\n"
+                                                  "2CMR/+ov0jRdXRa9iojCa3cNVc2KKg76Aci07f+jgZ0wgZowCQYDVR0TBAIwADAd\r\n"
+                                                  "BgNVHQ4EFgQUUGGlj9QH2deCAQzlZX+MY0anE74wbgYDVR0jBGcwZYAUnW0gJEkB\r\n"
+                                                  "PyvLeLUZvH4kydv7NnyhQqRAMD4xCzAJBgNVBAYTAk5MMREwDwYDVQQKEwhQb2xh\r\n"
+                                                  "clNTTDEcMBoGA1UEAxMTUG9sYXJzc2wgVGVzdCBFQyBDQYIJAMFD4n5iQ8zoMAoG\r\n"
+                                                  "CCqGSM49BAMCA2gAMGUCMQCaLFzXptui5WQN8LlO3ddh1hMxx6tzgLvT03MTVK2S\r\n"
+                                                  "C12r0Lz3ri/moSEpNZWqPjkCMCE2f53GXcYLqyfyJR078c/xNSUU5+Xxl7VZ414V\r\n"
+                                                  "fGa5kHvHARBPc8YAIVIqDvHH1Q==\r\n"
+                                                  "-----END CERTIFICATE-----\r\n";
+    static constexpr const size_t sSrvPemLength = 813; // includes NUL byte
+
+    static constexpr const char  *sSrvKey       = "-----BEGIN EC PRIVATE KEY-----\r\n"
+                                                  "MHcCAQEEIPEqEyB2AnCoPL/9U/YDHvdqXYbIogTywwyp6/UfDw6noAoGCCqGSM49\r\n"
+                                                  "AwEHoUQDQgAEN8xW2XYJHlpyPsdZLf8gbu58+QaRdNCtFLX3aCJZYpJO5QDYIxH/\r\n"
+                                                  "6i/SNF1dFr2KiMJrdw1VzYoqDvoByLTt/w==\r\n"
+                                                  "-----END EC PRIVATE KEY-----\r\n";
+    static constexpr const size_t sSrvKeyLength = 233; // includes NUL byte
+
+    static constexpr const char  *sEcjpakePassword       = "TLS-over-TCPlp";
+    static constexpr const size_t sEcjpakePasswordLength = 14;
+    static const int              sCipherSuites[];
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
 };
 
 } // namespace Cli
diff --git a/src/cli/cli_udp.cpp b/src/cli/cli_udp.cpp
index 62ad057..d5f3792 100644
--- a/src/cli/cli_udp.cpp
+++ b/src/cli/cli_udp.cpp
@@ -34,6 +34,7 @@
 #include "cli_udp.hpp"
 
 #include <openthread/message.h>
+#include <openthread/nat64.h>
 #include <openthread/udp.h>
 
 #include "cli/cli.hpp"
@@ -42,28 +43,14 @@
 namespace ot {
 namespace Cli {
 
-constexpr UdpExample::Command UdpExample::sCommands[];
-
-UdpExample::UdpExample(Output &aOutput)
-    : OutputWrapper(aOutput)
+UdpExample::UdpExample(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+    : Output(aInstance, aOutputImplementer)
     , mLinkSecurityEnabled(true)
 {
     memset(&mSocket, 0, sizeof(mSocket));
 }
 
-otError UdpExample::ProcessHelp(Arg aArgs[])
-{
-    OT_UNUSED_VARIABLE(aArgs);
-
-    for (const Command &command : sCommands)
-    {
-        OutputLine(command.mName);
-    }
-
-    return OT_ERROR_NONE;
-}
-
-otError UdpExample::ProcessBind(Arg aArgs[])
+template <> otError UdpExample::Process<Cmd("bind")>(Arg aArgs[])
 {
     otError           error;
     otSockAddr        sockaddr;
@@ -90,12 +77,20 @@
     return error;
 }
 
-otError UdpExample::ProcessConnect(Arg aArgs[])
+template <> otError UdpExample::Process<Cmd("connect")>(Arg aArgs[])
 {
     otError    error;
     otSockAddr sockaddr;
+    bool       nat64SynthesizedAddress;
 
-    SuccessOrExit(error = aArgs[0].ParseAsIp6Address(sockaddr.mAddress));
+    SuccessOrExit(
+        error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], sockaddr.mAddress, nat64SynthesizedAddress));
+    if (nat64SynthesizedAddress)
+    {
+        OutputFormat("Connecting to synthesized IPv6 address: ");
+        OutputIp6AddressLine(sockaddr.mAddress);
+    }
+
     SuccessOrExit(error = aArgs[1].ParseAsUint16(sockaddr.mPort));
     VerifyOrExit(aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
 
@@ -105,14 +100,14 @@
     return error;
 }
 
-otError UdpExample::ProcessClose(Arg aArgs[])
+template <> otError UdpExample::Process<Cmd("close")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
 
     return otUdpClose(GetInstancePtr(), &mSocket);
 }
 
-otError UdpExample::ProcessOpen(Arg aArgs[])
+template <> otError UdpExample::Process<Cmd("open")>(Arg aArgs[])
 {
     OT_UNUSED_VARIABLE(aArgs);
 
@@ -125,10 +120,10 @@
     return error;
 }
 
-otError UdpExample::ProcessSend(Arg aArgs[])
+template <> otError UdpExample::Process<Cmd("send")>(Arg aArgs[])
 {
     otError           error   = OT_ERROR_NONE;
-    otMessage *       message = nullptr;
+    otMessage        *message = nullptr;
     otMessageInfo     messageInfo;
     otMessageSettings messageSettings = {mLinkSecurityEnabled, OT_MESSAGE_PRIORITY_NORMAL};
 
@@ -143,7 +138,16 @@
 
     if (!aArgs[2].IsEmpty())
     {
-        SuccessOrExit(error = aArgs[0].ParseAsIp6Address(messageInfo.mPeerAddr));
+        bool nat64SynthesizedAddress;
+
+        SuccessOrExit(error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], messageInfo.mPeerAddr,
+                                                             nat64SynthesizedAddress));
+        if (nat64SynthesizedAddress)
+        {
+            OutputFormat("Sending to synthesized IPv6 address: ");
+            OutputIp6AddressLine(messageInfo.mPeerAddr);
+        }
+
         SuccessOrExit(error = aArgs[1].ParseAsUint16(messageInfo.mPeerPort));
         aArgs += 2;
     }
@@ -165,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
     {
@@ -193,7 +197,7 @@
     return error;
 }
 
-otError UdpExample::ProcessLinkSecurity(Arg aArgs[])
+template <> otError UdpExample::Process<Cmd("linksecurity")>(Arg aArgs[])
 {
     otError error = OT_ERROR_NONE;
 
@@ -239,7 +243,7 @@
     return error;
 }
 
-otError UdpExample::PrepareHexStringPaylod(otMessage &aMessage, const char *aHexString)
+otError UdpExample::PrepareHexStringPayload(otMessage &aMessage, const char *aHexString)
 {
     enum : uint8_t
     {
@@ -268,17 +272,29 @@
 
 otError UdpExample::Process(Arg aArgs[])
 {
-    otError        error = OT_ERROR_INVALID_ARGS;
-    const Command *command;
-
-    if (aArgs[0].IsEmpty())
-    {
-        IgnoreError(ProcessHelp(aArgs));
-        ExitNow();
+#define CmdEntry(aCommandString)                                  \
+    {                                                             \
+        aCommandString, &UdpExample::Process<Cmd(aCommandString)> \
     }
 
-    command = BinarySearch::Find(aArgs[0].GetCString(), sCommands);
-    VerifyOrExit(command != nullptr, error = OT_ERROR_INVALID_COMMAND);
+    static constexpr Command kCommands[] = {
+        CmdEntry("bind"),         CmdEntry("close"), CmdEntry("connect"),
+        CmdEntry("linksecurity"), CmdEntry("open"),  CmdEntry("send"),
+    };
+
+    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);
 
diff --git a/src/cli/cli_udp.hpp b/src/cli/cli_udp.hpp
index bd8ba54..55996c7 100644
--- a/src/cli/cli_udp.hpp
+++ b/src/cli/cli_udp.hpp
@@ -47,7 +47,7 @@
  * This class implements a CLI-based UDP example.
  *
  */
-class UdpExample : private OutputWrapper
+class UdpExample : private Output
 {
 public:
     typedef Utils::CmdLineParser::Arg Arg;
@@ -55,10 +55,11 @@
     /**
      * Constructor
      *
-     * @param[in]  aOutput The CLI console output context.
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
      *
      */
-    explicit UdpExample(Output &aOutput);
+    UdpExample(otInstance *aInstance, OutputImplementer &aOutputImplementer);
 
     /**
      * This method interprets a list of CLI arguments.
@@ -71,32 +72,14 @@
 private:
     using Command = CommandEntry<UdpExample>;
 
-    otError ProcessHelp(Arg aArgs[]);
-    otError ProcessBind(Arg aArgs[]);
-    otError ProcessClose(Arg aArgs[]);
-    otError ProcessConnect(Arg aArgs[]);
-    otError ProcessOpen(Arg aArgs[]);
-    otError ProcessSend(Arg aArgs[]);
-    otError ProcessLinkSecurity(Arg aArgs[]);
+    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);
 
-    static constexpr Command sCommands[] = {
-        {"bind", &UdpExample::ProcessBind},
-        {"close", &UdpExample::ProcessClose},
-        {"connect", &UdpExample::ProcessConnect},
-        {"help", &UdpExample::ProcessHelp},
-        {"linksecurity", &UdpExample::ProcessLinkSecurity},
-        {"open", &UdpExample::ProcessOpen},
-        {"send", &UdpExample::ProcessSend},
-    };
-
-    static_assert(BinarySearch::IsSorted(sCommands), "Command Table is not sorted");
-
     bool        mLinkSecurityEnabled;
     otUdpSocket mSocket;
 };
diff --git a/src/cli/ftd.cmake b/src/cli/ftd.cmake
index a101da3..22f1e11 100644
--- a/src/cli/ftd.cmake
+++ b/src/cli/ftd.cmake
@@ -46,5 +46,6 @@
         openthread-ftd
     PRIVATE
         ${OT_MBEDTLS}
+        ot-config-ftd
         ot-config
 )
diff --git a/src/cli/mtd.cmake b/src/cli/mtd.cmake
index c8c4230..3a875c3 100644
--- a/src/cli/mtd.cmake
+++ b/src/cli/mtd.cmake
@@ -46,5 +46,6 @@
         openthread-mtd
     PRIVATE
         ${OT_MBEDTLS}
+        ot-config-mtd
         ot-config
 )
diff --git a/src/cli/radio.cmake b/src/cli/radio.cmake
index 3e7354a..6fa0b91 100644
--- a/src/cli/radio.cmake
+++ b/src/cli/radio.cmake
@@ -55,5 +55,6 @@
         openthread-radio
     PRIVATE
         ${OT_MBEDTLS_RCP}
+        ot-config-radio
         ot-config
 )
diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn
index 299059f..d7f9f63 100644
--- a/src/core/BUILD.gn
+++ b/src/core/BUILD.gn
@@ -39,6 +39,8 @@
       defines += [ "OPENTHREAD_CONFIG_THREAD_VERSION=OT_THREAD_VERSION_1_2" ]
     } else if (openthread_config_thread_version == "1.3") {
       defines += [ "OPENTHREAD_CONFIG_THREAD_VERSION=OT_THREAD_VERSION_1_3" ]
+    } else if (openthread_config_thread_version == "1.3.1") {
+      defines += [ "OPENTHREAD_CONFIG_THREAD_VERSION=OT_THREAD_VERSION_1_3_1" ]
     } else if (openthread_config_thread_version != "") {
       assert(false,
              "Unrecognized Thread version: ${openthread_config_thread_version}")
@@ -98,10 +100,6 @@
       defines += [ "OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE=1" ]
     }
 
-    if (openthread_config_child_supervision_enable) {
-      defines += [ "OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE=1" ]
-    }
-
     if (openthread_config_coap_api_enable) {
       defines += [ "OPENTHREAD_CONFIG_COAP_API_ENABLE=1" ]
     }
@@ -170,10 +168,6 @@
       defines += [ "OPENTHREAD_CONFIG_JOINER_ENABLE=1" ]
     }
 
-    if (openthread_config_legacy_enable) {
-      defines += [ "OPENTHREAD_CONFIG_LEGACY_ENABLE=1" ]
-    }
-
     if (openthread_config_link_metrics_initiator_enable) {
       defines += [ "DOPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE=1" ]
     }
@@ -202,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" ]
     }
@@ -334,8 +329,10 @@
   "api/link_metrics_api.cpp",
   "api/link_raw_api.cpp",
   "api/logging_api.cpp",
+  "api/mesh_diag_api.cpp",
   "api/message_api.cpp",
   "api/multi_radio_api.cpp",
+  "api/nat64_api.cpp",
   "api/netdata_api.cpp",
   "api/netdata_publisher_api.cpp",
   "api/netdiag_api.cpp",
@@ -350,6 +347,7 @@
   "api/srp_server_api.cpp",
   "api/tasklet_api.cpp",
   "api/tcp_api.cpp",
+  "api/tcp_ext_api.cpp",
   "api/thread_api.cpp",
   "api/thread_ftd_api.cpp",
   "api/trel_api.cpp",
@@ -384,6 +382,7 @@
   "common/binary_search.cpp",
   "common/binary_search.hpp",
   "common/bit_vector.hpp",
+  "common/callback.hpp",
   "common/clearable.hpp",
   "common/code_utils.hpp",
   "common/const_cast.hpp",
@@ -424,10 +423,13 @@
   "common/non_copyable.hpp",
   "common/notifier.cpp",
   "common/notifier.hpp",
+  "common/num_utils.hpp",
   "common/numeric_limits.hpp",
   "common/owned_ptr.hpp",
   "common/owning_list.hpp",
   "common/pool.hpp",
+  "common/preference.cpp",
+  "common/preference.hpp",
   "common/ptr_wrapper.hpp",
   "common/random.cpp",
   "common/random.hpp",
@@ -458,17 +460,13 @@
   "crypto/aes_ecb.hpp",
   "crypto/context_size.hpp",
   "crypto/crypto_platform.cpp",
-  "crypto/ecdsa.cpp",
   "crypto/ecdsa.hpp",
-  "crypto/ecdsa_tinycrypt.cpp",
   "crypto/hkdf_sha256.cpp",
   "crypto/hkdf_sha256.hpp",
   "crypto/hmac_sha256.cpp",
   "crypto/hmac_sha256.hpp",
   "crypto/mbedtls.cpp",
   "crypto/mbedtls.hpp",
-  "crypto/pbkdf2_cmac.cpp",
-  "crypto/pbkdf2_cmac.hpp",
   "crypto/sha256.cpp",
   "crypto/sha256.hpp",
   "crypto/storage.cpp",
@@ -544,6 +542,7 @@
   "net/dns_client.hpp",
   "net/dns_dso.cpp",
   "net/dns_dso.hpp",
+  "net/dns_platform.cpp",
   "net/dns_types.cpp",
   "net/dns_types.hpp",
   "net/dnssd_server.cpp",
@@ -563,6 +562,8 @@
   "net/ip6_mpl.cpp",
   "net/ip6_mpl.hpp",
   "net/ip6_types.hpp",
+  "net/nat64_translator.cpp",
+  "net/nat64_translator.hpp",
   "net/nd6.cpp",
   "net/nd6.hpp",
   "net/nd_agent.cpp",
@@ -579,6 +580,8 @@
   "net/srp_server.hpp",
   "net/tcp6.cpp",
   "net/tcp6.hpp",
+  "net/tcp6_ext.cpp",
+  "net/tcp6_ext.hpp",
   "net/udp6.cpp",
   "net/udp6.hpp",
   "radio/max_power_table.hpp",
@@ -601,6 +604,8 @@
   "thread/anycast_locator.cpp",
   "thread/anycast_locator.hpp",
   "thread/child_mask.hpp",
+  "thread/child_supervision.cpp",
+  "thread/child_supervision.hpp",
   "thread/child_table.cpp",
   "thread/child_table.hpp",
   "thread/csl_tx_scheduler.cpp",
@@ -619,6 +624,8 @@
   "thread/link_metrics.cpp",
   "thread/link_metrics.hpp",
   "thread/link_metrics_tlvs.hpp",
+  "thread/link_metrics_types.cpp",
+  "thread/link_metrics_types.hpp",
   "thread/link_quality.cpp",
   "thread/link_quality.hpp",
   "thread/lowpan.cpp",
@@ -631,6 +638,7 @@
   "thread/mle.hpp",
   "thread/mle_router.cpp",
   "thread/mle_router.hpp",
+  "thread/mle_tlvs.cpp",
   "thread/mle_tlvs.hpp",
   "thread/mle_types.cpp",
   "thread/mle_types.hpp",
@@ -679,12 +687,11 @@
   "thread/topology.hpp",
   "thread/uri_paths.cpp",
   "thread/uri_paths.hpp",
+  "thread/version.hpp",
   "utils/channel_manager.cpp",
   "utils/channel_manager.hpp",
   "utils/channel_monitor.cpp",
   "utils/channel_monitor.hpp",
-  "utils/child_supervision.cpp",
-  "utils/child_supervision.hpp",
   "utils/flash.cpp",
   "utils/flash.hpp",
   "utils/heap.cpp",
@@ -693,12 +700,16 @@
   "utils/history_tracker.hpp",
   "utils/jam_detector.cpp",
   "utils/jam_detector.hpp",
+  "utils/mesh_diag.cpp",
+  "utils/mesh_diag.hpp",
   "utils/otns.cpp",
   "utils/otns.hpp",
   "utils/parse_cmdline.cpp",
   "utils/parse_cmdline.hpp",
   "utils/ping_sender.cpp",
   "utils/ping_sender.hpp",
+  "utils/power_calibration.cpp",
+  "utils/power_calibration.hpp",
   "utils/slaac_address.cpp",
   "utils/slaac_address.hpp",
   "utils/srp_client_buffers.cpp",
@@ -716,6 +727,8 @@
   "common/binary_search.cpp",
   "common/binary_search.hpp",
   "common/error.hpp",
+  "common/frame_builder.cpp",
+  "common/frame_builder.hpp",
   "common/instance.cpp",
   "common/log.cpp",
   "common/random.cpp",
@@ -738,6 +751,7 @@
   "radio/radio_platform.cpp",
   "thread/link_quality.cpp",
   "utils/parse_cmdline.cpp",
+  "utils/power_calibration.cpp",
 ]
 
 header_pattern = [
@@ -752,7 +766,9 @@
   public = [
     "config/announce_sender.h",
     "config/backbone_router.h",
+    "config/border_agent.h",
     "config/border_router.h",
+    "config/border_routing.h",
     "config/channel_manager.h",
     "config/channel_monitor.h",
     "config/child_supervision.h",
@@ -774,13 +790,17 @@
     "config/link_raw.h",
     "config/logging.h",
     "config/mac.h",
+    "config/mesh_diag.h",
     "config/misc.h",
     "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",
     "config/platform.h",
+    "config/power_calibration.h",
     "config/radio_link.h",
     "config/sntp_client.h",
     "config/srp_client.h",
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index ce36689..eb1279f 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -61,8 +61,10 @@
     api/link_metrics_api.cpp
     api/link_raw_api.cpp
     api/logging_api.cpp
+    api/mesh_diag_api.cpp
     api/message_api.cpp
     api/multi_radio_api.cpp
+    api/nat64_api.cpp
     api/netdata_api.cpp
     api/netdata_publisher_api.cpp
     api/netdiag_api.cpp
@@ -77,6 +79,7 @@
     api/srp_server_api.cpp
     api/tasklet_api.cpp
     api/tcp_api.cpp
+    api/tcp_ext_api.cpp
     api/thread_api.cpp
     api/thread_ftd_api.cpp
     api/trel_api.cpp
@@ -106,6 +109,7 @@
     common/log.cpp
     common/message.cpp
     common/notifier.cpp
+    common/preference.cpp
     common/random.cpp
     common/settings.cpp
     common/string.cpp
@@ -118,12 +122,9 @@
     crypto/aes_ccm.cpp
     crypto/aes_ecb.cpp
     crypto/crypto_platform.cpp
-    crypto/ecdsa.cpp
-    crypto/ecdsa_tinycrypt.cpp
     crypto/hkdf_sha256.cpp
     crypto/hmac_sha256.cpp
     crypto/mbedtls.cpp
-    crypto/pbkdf2_cmac.cpp
     crypto/sha256.cpp
     crypto/storage.cpp
     diags/factory_diags.cpp
@@ -162,6 +163,7 @@
     net/dhcp6_server.cpp
     net/dns_client.cpp
     net/dns_dso.cpp
+    net/dns_platform.cpp
     net/dns_types.cpp
     net/dnssd_server.cpp
     net/icmp6.cpp
@@ -171,6 +173,7 @@
     net/ip6_filter.cpp
     net/ip6_headers.cpp
     net/ip6_mpl.cpp
+    net/nat64_translator.cpp
     net/nd6.cpp
     net/nd_agent.cpp
     net/netif.cpp
@@ -179,6 +182,7 @@
     net/srp_client.cpp
     net/srp_server.cpp
     net/tcp6.cpp
+    net/tcp6_ext.cpp
     net/udp6.cpp
     radio/radio.cpp
     radio/radio_callbacks.cpp
@@ -190,6 +194,7 @@
     thread/announce_begin_server.cpp
     thread/announce_sender.cpp
     thread/anycast_locator.cpp
+    thread/child_supervision.cpp
     thread/child_table.cpp
     thread/csl_tx_scheduler.cpp
     thread/discover_scanner.cpp
@@ -198,6 +203,7 @@
     thread/indirect_sender.cpp
     thread/key_manager.cpp
     thread/link_metrics.cpp
+    thread/link_metrics_types.cpp
     thread/link_quality.cpp
     thread/lowpan.cpp
     thread/mesh_forwarder.cpp
@@ -205,6 +211,7 @@
     thread/mesh_forwarder_mtd.cpp
     thread/mle.cpp
     thread/mle_router.cpp
+    thread/mle_tlvs.cpp
     thread/mle_types.cpp
     thread/mlr_manager.cpp
     thread/neighbor_table.cpp
@@ -229,14 +236,15 @@
     thread/uri_paths.cpp
     utils/channel_manager.cpp
     utils/channel_monitor.cpp
-    utils/child_supervision.cpp
     utils/flash.cpp
     utils/heap.cpp
     utils/history_tracker.cpp
     utils/jam_detector.cpp
+    utils/mesh_diag.cpp
     utils/otns.cpp
     utils/parse_cmdline.cpp
     utils/ping_sender.cpp
+    utils/power_calibration.cpp
     utils/slaac_address.cpp
     utils/srp_client_buffers.cpp
 )
@@ -251,6 +259,7 @@
     api/tasklet_api.cpp
     common/binary_search.cpp
     common/error.cpp
+    common/frame_builder.cpp
     common/instance.cpp
     common/log.cpp
     common/random.cpp
@@ -273,6 +282,7 @@
     radio/radio_platform.cpp
     thread/link_quality.cpp
     utils/parse_cmdline.cpp
+    utils/power_calibration.cpp
 )
 
 set(OT_VENDOR_EXTENSION "" CACHE STRING "specify a C++ source file built as part of OpenThread core library")
diff --git a/src/core/Makefile.am b/src/core/Makefile.am
index 7b180ea..92c2bf6 100644
--- a/src/core/Makefile.am
+++ b/src/core/Makefile.am
@@ -151,8 +151,10 @@
     api/link_metrics_api.cpp                      \
     api/link_raw_api.cpp                          \
     api/logging_api.cpp                           \
+    api/mesh_diag_api.cpp                         \
     api/message_api.cpp                           \
     api/multi_radio_api.cpp                       \
+    api/nat64_api.cpp                             \
     api/netdata_api.cpp                           \
     api/netdata_publisher_api.cpp                 \
     api/netdiag_api.cpp                           \
@@ -167,6 +169,7 @@
     api/srp_server_api.cpp                        \
     api/tasklet_api.cpp                           \
     api/tcp_api.cpp                               \
+    api/tcp_ext_api.cpp                           \
     api/thread_api.cpp                            \
     api/thread_ftd_api.cpp                        \
     api/trel_api.cpp                              \
@@ -196,6 +199,7 @@
     common/log.cpp                                \
     common/message.cpp                            \
     common/notifier.cpp                           \
+    common/preference.cpp                         \
     common/random.cpp                             \
     common/settings.cpp                           \
     common/string.cpp                             \
@@ -208,12 +212,9 @@
     crypto/aes_ccm.cpp                            \
     crypto/aes_ecb.cpp                            \
     crypto/crypto_platform.cpp                    \
-    crypto/ecdsa.cpp                              \
-    crypto/ecdsa_tinycrypt.cpp                    \
     crypto/hkdf_sha256.cpp                        \
     crypto/hmac_sha256.cpp                        \
     crypto/mbedtls.cpp                            \
-    crypto/pbkdf2_cmac.cpp                        \
     crypto/sha256.cpp                             \
     crypto/storage.cpp                            \
     diags/factory_diags.cpp                       \
@@ -252,6 +253,7 @@
     net/dhcp6_server.cpp                          \
     net/dns_client.cpp                            \
     net/dns_dso.cpp                               \
+    net/dns_platform.cpp                          \
     net/dns_types.cpp                             \
     net/dnssd_server.cpp                          \
     net/icmp6.cpp                                 \
@@ -261,6 +263,7 @@
     net/ip6_filter.cpp                            \
     net/ip6_headers.cpp                           \
     net/ip6_mpl.cpp                               \
+    net/nat64_translator.cpp                      \
     net/nd6.cpp                                   \
     net/nd_agent.cpp                              \
     net/netif.cpp                                 \
@@ -269,6 +272,7 @@
     net/srp_client.cpp                            \
     net/srp_server.cpp                            \
     net/tcp6.cpp                                  \
+    net/tcp6_ext.cpp                              \
     net/udp6.cpp                                  \
     radio/radio.cpp                               \
     radio/radio_callbacks.cpp                     \
@@ -280,6 +284,7 @@
     thread/announce_begin_server.cpp              \
     thread/announce_sender.cpp                    \
     thread/anycast_locator.cpp                    \
+    thread/child_supervision.cpp                  \
     thread/child_table.cpp                        \
     thread/csl_tx_scheduler.cpp                   \
     thread/discover_scanner.cpp                   \
@@ -288,6 +293,7 @@
     thread/indirect_sender.cpp                    \
     thread/key_manager.cpp                        \
     thread/link_metrics.cpp                       \
+    thread/link_metrics_types.cpp                 \
     thread/link_quality.cpp                       \
     thread/lowpan.cpp                             \
     thread/mesh_forwarder.cpp                     \
@@ -295,6 +301,7 @@
     thread/mesh_forwarder_mtd.cpp                 \
     thread/mle.cpp                                \
     thread/mle_router.cpp                         \
+    thread/mle_tlvs.cpp                           \
     thread/mle_types.cpp                          \
     thread/mlr_manager.cpp                        \
     thread/neighbor_table.cpp                     \
@@ -319,14 +326,15 @@
     thread/uri_paths.cpp                          \
     utils/channel_manager.cpp                     \
     utils/channel_monitor.cpp                     \
-    utils/child_supervision.cpp                   \
     utils/flash.cpp                               \
     utils/heap.cpp                                \
     utils/history_tracker.cpp                     \
     utils/jam_detector.cpp                        \
+    utils/mesh_diag.cpp                           \
     utils/otns.cpp                                \
     utils/parse_cmdline.cpp                       \
     utils/ping_sender.cpp                         \
+    utils/power_calibration.cpp                   \
     utils/slaac_address.cpp                       \
     utils/srp_client_buffers.cpp                  \
     $(NULL)
@@ -341,6 +349,7 @@
     api/tasklet_api.cpp                      \
     common/binary_search.cpp                 \
     common/error.cpp                         \
+    common/frame_builder.cpp                 \
     common/instance.cpp                      \
     common/log.cpp                           \
     common/random.cpp                        \
@@ -363,6 +372,7 @@
     radio/radio_platform.cpp                 \
     thread/link_quality.cpp                  \
     utils/parse_cmdline.cpp                  \
+    utils/power_calibration.cpp              \
     $(NULL)
 
 EXTRA_DIST                                 = \
@@ -429,6 +439,7 @@
     common/as_core_type.hpp                       \
     common/binary_search.hpp                      \
     common/bit_vector.hpp                         \
+    common/callback.hpp                           \
     common/clearable.hpp                          \
     common/code_utils.hpp                         \
     common/const_cast.hpp                         \
@@ -457,10 +468,12 @@
     common/new.hpp                                \
     common/non_copyable.hpp                       \
     common/notifier.hpp                           \
+    common/num_utils.hpp                          \
     common/numeric_limits.hpp                     \
     common/owned_ptr.hpp                          \
     common/owning_list.hpp                        \
     common/pool.hpp                               \
+    common/preference.hpp                         \
     common/ptr_wrapper.hpp                        \
     common/random.hpp                             \
     common/retain_ptr.hpp                         \
@@ -478,7 +491,9 @@
     common/uptime.hpp                             \
     config/announce_sender.h                      \
     config/backbone_router.h                      \
+    config/border_agent.h                         \
     config/border_router.h                        \
+    config/border_routing.h                       \
     config/channel_manager.h                      \
     config/channel_monitor.h                      \
     config/child_supervision.h                    \
@@ -500,13 +515,17 @@
     config/link_raw.h                             \
     config/logging.h                              \
     config/mac.h                                  \
+    config/mesh_diag.h                            \
     config/misc.h                                 \
     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                          \
     config/platform.h                             \
+    config/power_calibration.h                    \
     config/radio_link.h                           \
     config/sntp_client.h                          \
     config/srp_client.h                           \
@@ -520,7 +539,6 @@
     crypto/hkdf_sha256.hpp                        \
     crypto/hmac_sha256.hpp                        \
     crypto/mbedtls.hpp                            \
-    crypto/pbkdf2_cmac.hpp                        \
     crypto/sha256.hpp                             \
     crypto/storage.hpp                            \
     diags/factory_diags.hpp                       \
@@ -568,6 +586,7 @@
     net/ip6_headers.hpp                           \
     net/ip6_mpl.hpp                               \
     net/ip6_types.hpp                             \
+    net/nat64_translator.hpp                      \
     net/nd6.hpp                                   \
     net/nd_agent.hpp                              \
     net/netif.hpp                                 \
@@ -576,6 +595,7 @@
     net/srp_client.hpp                            \
     net/srp_server.hpp                            \
     net/tcp6.hpp                                  \
+    net/tcp6_ext.hpp                              \
     net/udp6.hpp                                  \
     openthread-core-config.h                      \
     radio/max_power_table.hpp                     \
@@ -588,6 +608,7 @@
     thread/announce_sender.hpp                    \
     thread/anycast_locator.hpp                    \
     thread/child_mask.hpp                         \
+    thread/child_supervision.hpp                  \
     thread/child_table.hpp                        \
     thread/csl_tx_scheduler.hpp                   \
     thread/discover_scanner.hpp                   \
@@ -598,6 +619,7 @@
     thread/key_manager.hpp                        \
     thread/link_metrics.hpp                       \
     thread/link_metrics_tlvs.hpp                  \
+    thread/link_metrics_types.hpp                 \
     thread/link_quality.hpp                       \
     thread/lowpan.hpp                             \
     thread/mesh_forwarder.hpp                     \
@@ -629,16 +651,18 @@
     thread/tmf.hpp                                \
     thread/topology.hpp                           \
     thread/uri_paths.hpp                          \
+    thread/version.hpp                            \
     utils/channel_manager.hpp                     \
     utils/channel_monitor.hpp                     \
-    utils/child_supervision.hpp                   \
     utils/flash.hpp                               \
     utils/heap.hpp                                \
     utils/history_tracker.hpp                     \
     utils/jam_detector.hpp                        \
+    utils/mesh_diag.hpp                           \
     utils/otns.hpp                                \
     utils/parse_cmdline.hpp                       \
     utils/ping_sender.hpp                         \
+    utils/power_calibration.hpp                   \
     utils/slaac_address.hpp                       \
     utils/srp_client_buffers.hpp                  \
     $(NULL)
diff --git a/src/core/api/backbone_router_api.cpp b/src/core/api/backbone_router_api.cpp
index 179249a..cedb378 100644
--- a/src/core/api/backbone_router_api.cpp
+++ b/src/core/api/backbone_router_api.cpp
@@ -44,7 +44,7 @@
 
 otError otBackboneRouterGetPrimary(otInstance *aInstance, otBackboneRouterConfig *aConfig)
 {
-    OT_ASSERT(aConfig != nullptr);
+    AssertPointerIsNotNull(aConfig);
 
     return AsCoreType(aInstance).Get<BackboneRouter::Leader>().GetConfig(*aConfig);
 }
diff --git a/src/core/api/backbone_router_ftd_api.cpp b/src/core/api/backbone_router_ftd_api.cpp
index d8e5169..2f3e4b3 100644
--- a/src/core/api/backbone_router_ftd_api.cpp
+++ b/src/core/api/backbone_router_ftd_api.cpp
@@ -50,19 +50,19 @@
 
 otBackboneRouterState otBackboneRouterGetState(otInstance *aInstance)
 {
-    return AsCoreType(aInstance).Get<BackboneRouter::Local>().GetState();
+    return MapEnum(AsCoreType(aInstance).Get<BackboneRouter::Local>().GetState());
 }
 
 void otBackboneRouterGetConfig(otInstance *aInstance, otBackboneRouterConfig *aConfig)
 {
-    OT_ASSERT(aConfig != nullptr);
+    AssertPointerIsNotNull(aConfig);
 
     AsCoreType(aInstance).Get<BackboneRouter::Local>().GetConfig(*aConfig);
 }
 
 otError otBackboneRouterSetConfig(otInstance *aInstance, const otBackboneRouterConfig *aConfig)
 {
-    OT_ASSERT(aConfig != nullptr);
+    AssertPointerIsNotNull(aConfig);
 
     return AsCoreType(aInstance).Get<BackboneRouter::Local>().SetConfig(*aConfig);
 }
@@ -84,49 +84,49 @@
 
 otError otBackboneRouterGetDomainPrefix(otInstance *aInstance, otBorderRouterConfig *aConfig)
 {
-    OT_ASSERT(aConfig != nullptr);
-
     return AsCoreType(aInstance).Get<BackboneRouter::Local>().GetDomainPrefix(AsCoreType(aConfig));
 }
 
-void otBackboneRouterSetDomainPrefixCallback(otInstance *                         aInstance,
+void otBackboneRouterSetDomainPrefixCallback(otInstance                          *aInstance,
                                              otBackboneRouterDomainPrefixCallback aCallback,
-                                             void *                               aContext)
+                                             void                                *aContext)
 {
     return AsCoreType(aInstance).Get<BackboneRouter::Local>().SetDomainPrefixCallback(aCallback, aContext);
 }
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-void otBackboneRouterSetNdProxyCallback(otInstance *                    aInstance,
+void otBackboneRouterSetNdProxyCallback(otInstance                     *aInstance,
                                         otBackboneRouterNdProxyCallback aCallback,
-                                        void *                          aContext)
+                                        void                           *aContext)
 {
     AsCoreType(aInstance).Get<BackboneRouter::NdProxyTable>().SetCallback(aCallback, aContext);
 }
 
-otError otBackboneRouterGetNdProxyInfo(otInstance *                 aInstance,
-                                       const otIp6Address *         aDua,
+otError otBackboneRouterGetNdProxyInfo(otInstance                  *aInstance,
+                                       const otIp6Address          *aDua,
                                        otBackboneRouterNdProxyInfo *aNdProxyInfo)
 {
+    AssertPointerIsNotNull(aNdProxyInfo);
+
     return AsCoreType(aInstance).Get<BackboneRouter::NdProxyTable>().GetInfo(
         reinterpret_cast<const Ip6::Address &>(*aDua), *aNdProxyInfo);
 }
 #endif // OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
-void otBackboneRouterSetMulticastListenerCallback(otInstance *                              aInstance,
+void otBackboneRouterSetMulticastListenerCallback(otInstance                               *aInstance,
                                                   otBackboneRouterMulticastListenerCallback aCallback,
-                                                  void *                                    aContext)
+                                                  void                                     *aContext)
 {
     AsCoreType(aInstance).Get<BackboneRouter::MulticastListenersTable>().SetCallback(aCallback, aContext);
 }
 
-otError otBackboneRouterMulticastListenerGetNext(otInstance *                           aInstance,
-                                                 otChildIp6AddressIterator *            aIterator,
+otError otBackboneRouterMulticastListenerGetNext(otInstance                            *aInstance,
+                                                 otChildIp6AddressIterator             *aIterator,
                                                  otBackboneRouterMulticastListenerInfo *aListenerInfo)
 {
-    OT_ASSERT(aIterator != nullptr);
-    OT_ASSERT(aListenerInfo != nullptr);
+    AssertPointerIsNotNull(aIterator);
+    AssertPointerIsNotNull(aListenerInfo);
 
     return AsCoreType(aInstance).Get<BackboneRouter::MulticastListenersTable>().GetNext(*aIterator, *aListenerInfo);
 }
@@ -134,7 +134,7 @@
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-void otBackboneRouterConfigNextDuaRegistrationResponse(otInstance *                    aInstance,
+void otBackboneRouterConfigNextDuaRegistrationResponse(otInstance                     *aInstance,
                                                        const otIp6InterfaceIdentifier *aMlIid,
                                                        uint8_t                         aStatus)
 {
@@ -159,17 +159,14 @@
 
 otError otBackboneRouterMulticastListenerAdd(otInstance *aInstance, const otIp6Address *aAddress, uint32_t aTimeout)
 {
-    OT_ASSERT(aAddress != nullptr);
-
     if (aTimeout == 0)
     {
-        BackboneRouter::BackboneRouterConfig config;
+        BackboneRouter::Config config;
         AsCoreType(aInstance).Get<BackboneRouter::Local>().GetConfig(config);
         aTimeout = config.mMlrTimeout;
     }
 
-    aTimeout =
-        aTimeout > static_cast<uint32_t>(Mle::kMlrTimeoutMax) ? static_cast<uint32_t>(Mle::kMlrTimeoutMax) : aTimeout;
+    aTimeout = Min(aTimeout, Mle::kMlrTimeoutMax);
     aTimeout = Time::SecToMsec(aTimeout);
 
     return AsCoreType(aInstance).Get<BackboneRouter::MulticastListenersTable>().Add(AsCoreType(aAddress),
diff --git a/src/core/api/border_agent_api.cpp b/src/core/api/border_agent_api.cpp
index a6ccea1..70f39ba 100644
--- a/src/core/api/border_agent_api.cpp
+++ b/src/core/api/border_agent_api.cpp
@@ -42,6 +42,13 @@
 
 using namespace ot;
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+otError otBorderAgentGetId(otInstance *aInstance, uint8_t *aId, uint16_t *aLength)
+{
+    return AsCoreType(aInstance).Get<MeshCoP::BorderAgent>().GetId(aId, *aLength);
+}
+#endif
+
 otBorderAgentState otBorderAgentGetState(otInstance *aInstance)
 {
     return MapEnum(AsCoreType(aInstance).Get<MeshCoP::BorderAgent>().GetState());
diff --git a/src/core/api/border_router_api.cpp b/src/core/api/border_router_api.cpp
index 2ef3c4d..c3dd9de 100644
--- a/src/core/api/border_router_api.cpp
+++ b/src/core/api/border_router_api.cpp
@@ -53,8 +53,6 @@
 {
     Error error = kErrorNone;
 
-    OT_ASSERT(aConfig != nullptr);
-
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
     if (aConfig->mDp)
     {
@@ -73,8 +71,6 @@
 {
     Error error = kErrorNone;
 
-    OT_ASSERT(aPrefix != nullptr);
-
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
     error = AsCoreType(aInstance).Get<BackboneRouter::Local>().RemoveDomainPrefix(AsCoreType(aPrefix));
 
@@ -87,34 +83,30 @@
     return error;
 }
 
-otError otBorderRouterGetNextOnMeshPrefix(otInstance *           aInstance,
+otError otBorderRouterGetNextOnMeshPrefix(otInstance            *aInstance,
                                           otNetworkDataIterator *aIterator,
-                                          otBorderRouterConfig * aConfig)
+                                          otBorderRouterConfig  *aConfig)
 {
-    OT_ASSERT(aIterator != nullptr && aConfig != nullptr);
+    AssertPointerIsNotNull(aIterator);
 
     return AsCoreType(aInstance).Get<NetworkData::Local>().GetNextOnMeshPrefix(*aIterator, AsCoreType(aConfig));
 }
 
 otError otBorderRouterAddRoute(otInstance *aInstance, const otExternalRouteConfig *aConfig)
 {
-    OT_ASSERT(aConfig != nullptr);
-
     return AsCoreType(aInstance).Get<NetworkData::Local>().AddHasRoutePrefix(AsCoreType(aConfig));
 }
 
 otError otBorderRouterRemoveRoute(otInstance *aInstance, const otIp6Prefix *aPrefix)
 {
-    OT_ASSERT(aPrefix != nullptr);
-
     return AsCoreType(aInstance).Get<NetworkData::Local>().RemoveHasRoutePrefix(AsCoreType(aPrefix));
 }
 
-otError otBorderRouterGetNextRoute(otInstance *           aInstance,
+otError otBorderRouterGetNextRoute(otInstance            *aInstance,
                                    otNetworkDataIterator *aIterator,
                                    otExternalRouteConfig *aConfig)
 {
-    OT_ASSERT(aIterator != nullptr && aConfig != nullptr);
+    AssertPointerIsNotNull(aIterator);
 
     return AsCoreType(aInstance).Get<NetworkData::Local>().GetNextExternalRoute(*aIterator, AsCoreType(aConfig));
 }
diff --git a/src/core/api/border_routing_api.cpp b/src/core/api/border_routing_api.cpp
index 1d942c5..2731e02 100644
--- a/src/core/api/border_routing_api.cpp
+++ b/src/core/api/border_routing_api.cpp
@@ -52,6 +52,11 @@
     return AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().SetEnabled(aEnabled);
 }
 
+otBorderRoutingState otBorderRoutingGetState(otInstance *aInstance)
+{
+    return MapEnum(AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().GetState());
+}
+
 otRoutePreference otBorderRoutingGetRouteInfoOptionPreference(otInstance *aInstance)
 {
     return static_cast<otRoutePreference>(
@@ -64,6 +69,11 @@
         static_cast<NetworkData::RoutePreference>(aPreference));
 }
 
+void otBorderRoutingClearRouteInfoOptionPreference(otInstance *aInstance)
+{
+    AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().ClearRouteInfoOptionPreference();
+}
+
 otError otBorderRoutingGetOmrPrefix(otInstance *aInstance, otIp6Prefix *aPrefix)
 {
     return AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().GetOmrPrefix(AsCoreType(aPrefix));
@@ -74,6 +84,8 @@
     otError                                       error;
     BorderRouter::RoutingManager::RoutePreference preference;
 
+    AssertPointerIsNotNull(aPreference);
+
     SuccessOrExit(error = AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().GetFavoredOmrPrefix(
                       AsCoreType(aPrefix), preference));
     *aPreference = static_cast<otRoutePreference>(preference);
@@ -87,22 +99,49 @@
     return AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(AsCoreType(aPrefix));
 }
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
+otError otBorderRoutingGetFavoredOnLinkPrefix(otInstance *aInstance, otIp6Prefix *aPrefix)
+{
+    return AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().GetFavoredOnLinkPrefix(AsCoreType(aPrefix));
+}
+
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
 otError otBorderRoutingGetNat64Prefix(otInstance *aInstance, otIp6Prefix *aPrefix)
 {
     return AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().GetNat64Prefix(AsCoreType(aPrefix));
 }
+
+otError otBorderRoutingGetFavoredNat64Prefix(otInstance        *aInstance,
+                                             otIp6Prefix       *aPrefix,
+                                             otRoutePreference *aPreference)
+{
+    otError                                       error;
+    BorderRouter::RoutingManager::RoutePreference preference;
+
+    AssertPointerIsNotNull(aPreference);
+
+    SuccessOrExit(error = AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().GetFavoredNat64Prefix(
+                      AsCoreType(aPrefix), preference));
+    *aPreference = static_cast<otRoutePreference>(preference);
+
+exit:
+    return error;
+}
 #endif
 
 void otBorderRoutingPrefixTableInitIterator(otInstance *aInstance, otBorderRoutingPrefixTableIterator *aIterator)
 {
+    AssertPointerIsNotNull(aIterator);
+
     AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().InitPrefixTableIterator(*aIterator);
 }
 
-otError otBorderRoutingGetNextPrefixTableEntry(otInstance *                        aInstance,
+otError otBorderRoutingGetNextPrefixTableEntry(otInstance                         *aInstance,
                                                otBorderRoutingPrefixTableIterator *aIterator,
-                                               otBorderRoutingPrefixTableEntry *   aEntry)
+                                               otBorderRoutingPrefixTableEntry    *aEntry)
 {
+    AssertPointerIsNotNull(aIterator);
+    AssertPointerIsNotNull(aEntry);
+
     return AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().GetNextPrefixTableEntry(*aIterator, *aEntry);
 }
 
diff --git a/src/core/api/child_supervision_api.cpp b/src/core/api/child_supervision_api.cpp
index ab2dcec..d24a17d 100644
--- a/src/core/api/child_supervision_api.cpp
+++ b/src/core/api/child_supervision_api.cpp
@@ -33,8 +33,6 @@
 
 #include "openthread-core-config.h"
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-
 #include <openthread/child_supervision.h>
 
 #include "common/as_core_type.hpp"
@@ -42,28 +40,32 @@
 
 using namespace ot;
 
-#if OPENTHREAD_FTD
-
 uint16_t otChildSupervisionGetInterval(otInstance *aInstance)
 {
-    return AsCoreType(aInstance).Get<Utils::ChildSupervisor>().GetSupervisionInterval();
+    return AsCoreType(aInstance).Get<SupervisionListener>().GetInterval();
 }
 
 void otChildSupervisionSetInterval(otInstance *aInstance, uint16_t aInterval)
 {
-    AsCoreType(aInstance).Get<Utils::ChildSupervisor>().SetSupervisionInterval(aInterval);
+    AsCoreType(aInstance).Get<SupervisionListener>().SetInterval(aInterval);
 }
 
-#endif
-
 uint16_t otChildSupervisionGetCheckTimeout(otInstance *aInstance)
 {
-    return AsCoreType(aInstance).Get<Utils::SupervisionListener>().GetTimeout();
+    return AsCoreType(aInstance).Get<SupervisionListener>().GetTimeout();
 }
 
 void otChildSupervisionSetCheckTimeout(otInstance *aInstance, uint16_t aTimeout)
 {
-    AsCoreType(aInstance).Get<Utils::SupervisionListener>().SetTimeout(aTimeout);
+    AsCoreType(aInstance).Get<SupervisionListener>().SetTimeout(aTimeout);
 }
 
-#endif // OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
+uint16_t otChildSupervisionGetCheckFailureCounter(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<SupervisionListener>().GetCounter();
+}
+
+void otChildSupervisionResetCheckFailureCounter(otInstance *aInstance)
+{
+    AsCoreType(aInstance).Get<SupervisionListener>().ResetCounter();
+}
diff --git a/src/core/api/coap_api.cpp b/src/core/api/coap_api.cpp
index 4dfb35e..6c52cd2 100644
--- a/src/core/api/coap_api.cpp
+++ b/src/core/api/coap_api.cpp
@@ -55,7 +55,7 @@
 
 otError otCoapMessageInitResponse(otMessage *aResponse, const otMessage *aRequest, otCoapType aType, otCoapCode aCode)
 {
-    Coap::Message &      response = AsCoapMessage(aResponse);
+    Coap::Message       &response = AsCoapMessage(aResponse);
     const Coap::Message &request  = AsCoapMessage(aRequest);
 
     response.Init(MapEnum(aType), MapEnum(aCode));
@@ -129,10 +129,7 @@
     return AsCoapMessage(aMessage).AppendUriQueryOption(aUriQuery);
 }
 
-otError otCoapMessageSetPayloadMarker(otMessage *aMessage)
-{
-    return AsCoapMessage(aMessage).SetPayloadMarker();
-}
+otError otCoapMessageSetPayloadMarker(otMessage *aMessage) { return AsCoapMessage(aMessage).SetPayloadMarker(); }
 
 otCoapType otCoapMessageGetType(const otMessage *aMessage)
 {
@@ -144,30 +141,15 @@
     return static_cast<otCoapCode>(AsCoapMessage(aMessage).GetCode());
 }
 
-void otCoapMessageSetCode(otMessage *aMessage, otCoapCode aCode)
-{
-    AsCoapMessage(aMessage).SetCode(MapEnum(aCode));
-}
+void otCoapMessageSetCode(otMessage *aMessage, otCoapCode aCode) { AsCoapMessage(aMessage).SetCode(MapEnum(aCode)); }
 
-const char *otCoapMessageCodeToString(const otMessage *aMessage)
-{
-    return AsCoapMessage(aMessage).CodeToString();
-}
+const char *otCoapMessageCodeToString(const otMessage *aMessage) { return AsCoapMessage(aMessage).CodeToString(); }
 
-uint16_t otCoapMessageGetMessageId(const otMessage *aMessage)
-{
-    return AsCoapMessage(aMessage).GetMessageId();
-}
+uint16_t otCoapMessageGetMessageId(const otMessage *aMessage) { return AsCoapMessage(aMessage).GetMessageId(); }
 
-uint8_t otCoapMessageGetTokenLength(const otMessage *aMessage)
-{
-    return AsCoapMessage(aMessage).GetTokenLength();
-}
+uint8_t otCoapMessageGetTokenLength(const otMessage *aMessage) { return AsCoapMessage(aMessage).GetTokenLength(); }
 
-const uint8_t *otCoapMessageGetToken(const otMessage *aMessage)
-{
-    return AsCoapMessage(aMessage).GetToken();
-}
+const uint8_t *otCoapMessageGetToken(const otMessage *aMessage) { return AsCoapMessage(aMessage).GetToken(); }
 
 otError otCoapOptionIteratorInit(otCoapOptionIterator *aIterator, const otMessage *aMessage)
 {
@@ -217,12 +199,12 @@
 }
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-otError otCoapSendRequestBlockWiseWithParameters(otInstance *                aInstance,
-                                                 otMessage *                 aMessage,
-                                                 const otMessageInfo *       aMessageInfo,
+otError otCoapSendRequestBlockWiseWithParameters(otInstance                 *aInstance,
+                                                 otMessage                  *aMessage,
+                                                 const otMessageInfo        *aMessageInfo,
                                                  otCoapResponseHandler       aHandler,
-                                                 void *                      aContext,
-                                                 const otCoapTxParameters *  aTxParameters,
+                                                 void                       *aContext,
+                                                 const otCoapTxParameters   *aTxParameters,
                                                  otCoapBlockwiseTransmitHook aTransmitHook,
                                                  otCoapBlockwiseReceiveHook  aReceiveHook)
 {
@@ -243,11 +225,11 @@
 }
 #endif // OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
 
-otError otCoapSendRequestWithParameters(otInstance *              aInstance,
-                                        otMessage *               aMessage,
-                                        const otMessageInfo *     aMessageInfo,
+otError otCoapSendRequestWithParameters(otInstance               *aInstance,
+                                        otMessage                *aMessage,
+                                        const otMessageInfo      *aMessageInfo,
                                         otCoapResponseHandler     aHandler,
-                                        void *                    aContext,
+                                        void                     *aContext,
                                         const otCoapTxParameters *aTxParameters)
 {
     Error error;
@@ -271,10 +253,7 @@
     return AsCoreType(aInstance).GetApplicationCoap().Start(aPort);
 }
 
-otError otCoapStop(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).GetApplicationCoap().Stop();
-}
+otError otCoapStop(otInstance *aInstance) { return AsCoreType(aInstance).GetApplicationCoap().Stop(); }
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
 void otCoapAddBlockWiseResource(otInstance *aInstance, otCoapBlockwiseResource *aResource)
@@ -304,11 +283,11 @@
 }
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-otError otCoapSendResponseBlockWiseWithParameters(otInstance *                aInstance,
-                                                  otMessage *                 aMessage,
-                                                  const otMessageInfo *       aMessageInfo,
-                                                  const otCoapTxParameters *  aTxParameters,
-                                                  void *                      aContext,
+otError otCoapSendResponseBlockWiseWithParameters(otInstance                 *aInstance,
+                                                  otMessage                  *aMessage,
+                                                  const otMessageInfo        *aMessageInfo,
+                                                  const otCoapTxParameters   *aTxParameters,
+                                                  void                       *aContext,
                                                   otCoapBlockwiseTransmitHook aTransmitHook)
 {
     return AsCoreType(aInstance).GetApplicationCoap().SendMessage(AsCoapMessage(aMessage), AsCoreType(aMessageInfo),
@@ -317,9 +296,9 @@
 }
 #endif
 
-otError otCoapSendResponseWithParameters(otInstance *              aInstance,
-                                         otMessage *               aMessage,
-                                         const otMessageInfo *     aMessageInfo,
+otError otCoapSendResponseWithParameters(otInstance               *aInstance,
+                                         otMessage                *aMessage,
+                                         const otMessageInfo      *aMessageInfo,
                                          const otCoapTxParameters *aTxParameters)
 {
     return AsCoreType(aInstance).GetApplicationCoap().SendMessage(
diff --git a/src/core/api/coap_secure_api.cpp b/src/core/api/coap_secure_api.cpp
index 0a0c6aa..86f76b4 100644
--- a/src/core/api/coap_secure_api.cpp
+++ b/src/core/api/coap_secure_api.cpp
@@ -51,48 +51,47 @@
 }
 
 #ifdef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
-void otCoapSecureSetCertificate(otInstance *   aInstance,
+void otCoapSecureSetCertificate(otInstance    *aInstance,
                                 const uint8_t *aX509Cert,
                                 uint32_t       aX509Length,
                                 const uint8_t *aPrivateKey,
                                 uint32_t       aPrivateKeyLength)
 {
-    OT_ASSERT(aX509Cert != nullptr && aX509Length != 0 && aPrivateKey != nullptr && aPrivateKeyLength != 0);
-
     AsCoreType(aInstance).GetApplicationCoapSecure().SetCertificate(aX509Cert, aX509Length, aPrivateKey,
                                                                     aPrivateKeyLength);
 }
 
-void otCoapSecureSetCaCertificateChain(otInstance *   aInstance,
+void otCoapSecureSetCaCertificateChain(otInstance    *aInstance,
                                        const uint8_t *aX509CaCertificateChain,
                                        uint32_t       aX509CaCertChainLength)
 {
-    OT_ASSERT(aX509CaCertificateChain != nullptr && aX509CaCertChainLength != 0);
-
     AsCoreType(aInstance).GetApplicationCoapSecure().SetCaCertificateChain(aX509CaCertificateChain,
                                                                            aX509CaCertChainLength);
 }
 #endif // MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
 
 #ifdef MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
-void otCoapSecureSetPsk(otInstance *   aInstance,
+void otCoapSecureSetPsk(otInstance    *aInstance,
                         const uint8_t *aPsk,
                         uint16_t       aPskLength,
                         const uint8_t *aPskIdentity,
                         uint16_t       aPskIdLength)
 {
-    OT_ASSERT(aPsk != nullptr && aPskLength != 0 && aPskIdentity != nullptr && aPskIdLength != 0);
+    AssertPointerIsNotNull(aPsk);
+    AssertPointerIsNotNull(aPskIdentity);
 
     AsCoreType(aInstance).GetApplicationCoapSecure().SetPreSharedKey(aPsk, aPskLength, aPskIdentity, aPskIdLength);
 }
 #endif // MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
 
 #if defined(MBEDTLS_BASE64_C) && defined(MBEDTLS_SSL_KEEP_PEER_CERTIFICATE)
-otError otCoapSecureGetPeerCertificateBase64(otInstance *   aInstance,
+otError otCoapSecureGetPeerCertificateBase64(otInstance    *aInstance,
                                              unsigned char *aPeerCert,
-                                             size_t *       aCertLength,
+                                             size_t        *aCertLength,
                                              size_t         aCertBufferSize)
 {
+    AssertPointerIsNotNull(aPeerCert);
+
     return AsCoreType(aInstance).GetApplicationCoapSecure().GetPeerCertificateBase64(aPeerCert, aCertLength,
                                                                                      aCertBufferSize);
 }
@@ -103,18 +102,15 @@
     AsCoreType(aInstance).GetApplicationCoapSecure().SetSslAuthMode(aVerifyPeerCertificate);
 }
 
-otError otCoapSecureConnect(otInstance *                    aInstance,
-                            const otSockAddr *              aSockAddr,
+otError otCoapSecureConnect(otInstance                     *aInstance,
+                            const otSockAddr               *aSockAddr,
                             otHandleCoapSecureClientConnect aHandler,
-                            void *                          aContext)
+                            void                           *aContext)
 {
     return AsCoreType(aInstance).GetApplicationCoapSecure().Connect(AsCoreType(aSockAddr), aHandler, aContext);
 }
 
-void otCoapSecureDisconnect(otInstance *aInstance)
-{
-    AsCoreType(aInstance).GetApplicationCoapSecure().Disconnect();
-}
+void otCoapSecureDisconnect(otInstance *aInstance) { AsCoreType(aInstance).GetApplicationCoapSecure().Disconnect(); }
 
 bool otCoapSecureIsConnected(otInstance *aInstance)
 {
@@ -126,16 +122,13 @@
     return AsCoreType(aInstance).GetApplicationCoapSecure().IsConnectionActive();
 }
 
-void otCoapSecureStop(otInstance *aInstance)
-{
-    AsCoreType(aInstance).GetApplicationCoapSecure().Stop();
-}
+void otCoapSecureStop(otInstance *aInstance) { AsCoreType(aInstance).GetApplicationCoapSecure().Stop(); }
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-otError otCoapSecureSendRequestBlockWise(otInstance *                aInstance,
-                                         otMessage *                 aMessage,
+otError otCoapSecureSendRequestBlockWise(otInstance                 *aInstance,
+                                         otMessage                  *aMessage,
                                          otCoapResponseHandler       aHandler,
-                                         void *                      aContext,
+                                         void                       *aContext,
                                          otCoapBlockwiseTransmitHook aTransmitHook,
                                          otCoapBlockwiseReceiveHook  aReceiveHook)
 {
@@ -144,10 +137,10 @@
 }
 #endif
 
-otError otCoapSecureSendRequest(otInstance *          aInstance,
-                                otMessage *           aMessage,
+otError otCoapSecureSendRequest(otInstance           *aInstance,
+                                otMessage            *aMessage,
                                 otCoapResponseHandler aHandler,
-                                void *                aContext)
+                                void                 *aContext)
 {
     return AsCoreType(aInstance).GetApplicationCoapSecure().SendMessage(AsCoapMessage(aMessage), aHandler, aContext);
 }
@@ -174,9 +167,9 @@
     AsCoreType(aInstance).GetApplicationCoapSecure().RemoveResource(AsCoreType(aResource));
 }
 
-void otCoapSecureSetClientConnectedCallback(otInstance *                    aInstance,
+void otCoapSecureSetClientConnectedCallback(otInstance                     *aInstance,
                                             otHandleCoapSecureClientConnect aHandler,
-                                            void *                          aContext)
+                                            void                           *aContext)
 {
     AsCoreType(aInstance).GetApplicationCoapSecure().SetClientConnectedCallback(aHandler, aContext);
 }
@@ -187,10 +180,10 @@
 }
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-otError otCoapSecureSendResponseBlockWise(otInstance *                aInstance,
-                                          otMessage *                 aMessage,
-                                          const otMessageInfo *       aMessageInfo,
-                                          void *                      aContext,
+otError otCoapSecureSendResponseBlockWise(otInstance                 *aInstance,
+                                          otMessage                  *aMessage,
+                                          const otMessageInfo        *aMessageInfo,
+                                          void                       *aContext,
                                           otCoapBlockwiseTransmitHook aTransmitHook)
 {
     return AsCoreType(aInstance).GetApplicationCoapSecure().SendMessage(
diff --git a/src/core/api/commissioner_api.cpp b/src/core/api/commissioner_api.cpp
index 9c5b575..6831c87 100644
--- a/src/core/api/commissioner_api.cpp
+++ b/src/core/api/commissioner_api.cpp
@@ -42,10 +42,10 @@
 
 using namespace ot;
 
-otError otCommissionerStart(otInstance *                 aInstance,
+otError otCommissionerStart(otInstance                  *aInstance,
                             otCommissionerStateCallback  aStateCallback,
                             otCommissionerJoinerCallback aJoinerCallback,
-                            void *                       aCallbackContext)
+                            void                        *aCallbackContext)
 {
     return AsCoreType(aInstance).Get<MeshCoP::Commissioner>().Start(aStateCallback, aJoinerCallback, aCallbackContext);
 }
@@ -60,10 +60,7 @@
     return AsCoreType(aInstance).Get<MeshCoP::Commissioner>().SetId(aId);
 }
 
-otError otCommissionerStop(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<MeshCoP::Commissioner>().Stop();
-}
+otError otCommissionerStop(otInstance *aInstance) { return AsCoreType(aInstance).Get<MeshCoP::Commissioner>().Stop(); }
 
 otError otCommissionerAddJoiner(otInstance *aInstance, const otExtAddress *aEui64, const char *aPskd, uint32_t aTimeout)
 {
@@ -82,9 +79,9 @@
     return error;
 }
 
-otError otCommissionerAddJoinerWithDiscerner(otInstance *             aInstance,
+otError otCommissionerAddJoinerWithDiscerner(otInstance              *aInstance,
                                              const otJoinerDiscerner *aDiscerner,
-                                             const char *             aPskd,
+                                             const char              *aPskd,
                                              uint32_t                 aTimeout)
 {
     return AsCoreType(aInstance).Get<MeshCoP::Commissioner>().AddJoiner(AsCoreType(aDiscerner), aPskd, aTimeout);
@@ -127,7 +124,7 @@
     return AsCoreType(aInstance).Get<MeshCoP::Commissioner>().GetProvisioningUrl();
 }
 
-otError otCommissionerAnnounceBegin(otInstance *        aInstance,
+otError otCommissionerAnnounceBegin(otInstance         *aInstance,
                                     uint32_t            aChannelMask,
                                     uint8_t             aCount,
                                     uint16_t            aPeriod,
@@ -137,25 +134,25 @@
         aChannelMask, aCount, aPeriod, AsCoreType(aAddress));
 }
 
-otError otCommissionerEnergyScan(otInstance *                       aInstance,
+otError otCommissionerEnergyScan(otInstance                        *aInstance,
                                  uint32_t                           aChannelMask,
                                  uint8_t                            aCount,
                                  uint16_t                           aPeriod,
                                  uint16_t                           aScanDuration,
-                                 const otIp6Address *               aAddress,
+                                 const otIp6Address                *aAddress,
                                  otCommissionerEnergyReportCallback aCallback,
-                                 void *                             aContext)
+                                 void                              *aContext)
 {
     return AsCoreType(aInstance).Get<MeshCoP::Commissioner>().GetEnergyScanClient().SendQuery(
         aChannelMask, aCount, aPeriod, aScanDuration, AsCoreType(aAddress), aCallback, aContext);
 }
 
-otError otCommissionerPanIdQuery(otInstance *                        aInstance,
+otError otCommissionerPanIdQuery(otInstance                         *aInstance,
                                  uint16_t                            aPanId,
                                  uint32_t                            aChannelMask,
-                                 const otIp6Address *                aAddress,
+                                 const otIp6Address                 *aAddress,
                                  otCommissionerPanIdConflictCallback aCallback,
-                                 void *                              aContext)
+                                 void                               *aContext)
 {
     return AsCoreType(aInstance).Get<MeshCoP::Commissioner>().GetPanIdQueryClient().SendQuery(
         aPanId, aChannelMask, AsCoreType(aAddress), aCallback, aContext);
@@ -166,9 +163,9 @@
     return AsCoreType(aInstance).Get<MeshCoP::Commissioner>().SendMgmtCommissionerGetRequest(aTlvs, aLength);
 }
 
-otError otCommissionerSendMgmtSet(otInstance *                  aInstance,
+otError otCommissionerSendMgmtSet(otInstance                   *aInstance,
                                   const otCommissioningDataset *aDataset,
-                                  const uint8_t *               aTlvs,
+                                  const uint8_t                *aTlvs,
                                   uint8_t                       aLength)
 {
     return AsCoreType(aInstance).Get<MeshCoP::Commissioner>().SendMgmtCommissionerSetRequest(AsCoreType(aDataset),
diff --git a/src/core/api/crypto_api.cpp b/src/core/api/crypto_api.cpp
index 8a96d46..069596f 100644
--- a/src/core/api/crypto_api.cpp
+++ b/src/core/api/crypto_api.cpp
@@ -51,27 +51,31 @@
 {
     HmacSha256 hmac;
 
-    OT_ASSERT((aKey != nullptr) && (aBuf != nullptr) && (aHash != nullptr));
+    AssertPointerIsNotNull(aBuf);
 
     hmac.Start(AsCoreType(aKey));
     hmac.Update(aBuf, aBufLength);
-    hmac.Finish(ot::AsCoreType(aHash));
+    hmac.Finish(AsCoreType(aHash));
 }
 
 void otCryptoAesCcm(const otCryptoKey *aKey,
                     uint8_t            aTagLength,
-                    const void *       aNonce,
+                    const void        *aNonce,
                     uint8_t            aNonceLength,
-                    const void *       aHeader,
+                    const void        *aHeader,
                     uint32_t           aHeaderLength,
-                    void *             aPlainText,
-                    void *             aCipherText,
+                    void              *aPlainText,
+                    void              *aCipherText,
                     uint32_t           aLength,
                     bool               aEncrypt,
-                    void *             aTag)
+                    void              *aTag)
 {
     AesCcm aesCcm;
-    OT_ASSERT((aNonce != nullptr) && (aPlainText != nullptr) && (aCipherText != nullptr) && (aTag != nullptr));
+
+    AssertPointerIsNotNull(aNonce);
+    AssertPointerIsNotNull(aPlainText);
+    AssertPointerIsNotNull(aCipherText);
+    AssertPointerIsNotNull(aTag);
 
     aesCcm.SetKey(AsCoreType(aKey));
     aesCcm.Init(aHeaderLength, aLength, aTagLength, aNonce, aNonceLength);
@@ -85,17 +89,3 @@
     aesCcm.Payload(aPlainText, aCipherText, aLength, aEncrypt ? AesCcm::kEncrypt : AesCcm::kDecrypt);
     aesCcm.Finalize(aTag);
 }
-
-#if OPENTHREAD_CONFIG_ECDSA_ENABLE
-
-otError otCryptoEcdsaSign(uint8_t *      aOutput,
-                          uint16_t *     aOutputLength,
-                          const uint8_t *aInputHash,
-                          uint16_t       aInputHashLength,
-                          const uint8_t *aPrivateKey,
-                          uint16_t       aPrivateKeyLength)
-{
-    return Ecdsa::Sign(aOutput, *aOutputLength, aInputHash, aInputHashLength, aPrivateKey, aPrivateKeyLength);
-}
-
-#endif // OPENTHREAD_CONFIG_ECDSA_ENABLE
diff --git a/src/core/api/dataset_api.cpp b/src/core/api/dataset_api.cpp
index 457c783..7e4c0d4 100644
--- a/src/core/api/dataset_api.cpp
+++ b/src/core/api/dataset_api.cpp
@@ -49,111 +49,103 @@
 
 otError otDatasetGetActive(otInstance *aInstance, otOperationalDataset *aDataset)
 {
-    OT_ASSERT(aDataset != nullptr);
-
     return AsCoreType(aInstance).Get<MeshCoP::ActiveDatasetManager>().Read(AsCoreType(aDataset));
 }
 
 otError otDatasetGetActiveTlvs(otInstance *aInstance, otOperationalDatasetTlvs *aDataset)
 {
-    OT_ASSERT(aDataset != nullptr);
+    AssertPointerIsNotNull(aDataset);
 
     return AsCoreType(aInstance).Get<MeshCoP::ActiveDatasetManager>().Read(*aDataset);
 }
 
 otError otDatasetSetActive(otInstance *aInstance, const otOperationalDataset *aDataset)
 {
-    OT_ASSERT(aDataset != nullptr);
-
     return AsCoreType(aInstance).Get<MeshCoP::ActiveDatasetManager>().Save(AsCoreType(aDataset));
 }
 
 otError otDatasetSetActiveTlvs(otInstance *aInstance, const otOperationalDatasetTlvs *aDataset)
 {
-    OT_ASSERT(aDataset != nullptr);
+    AssertPointerIsNotNull(aDataset);
 
     return AsCoreType(aInstance).Get<MeshCoP::ActiveDatasetManager>().Save(*aDataset);
 }
 
 otError otDatasetGetPending(otInstance *aInstance, otOperationalDataset *aDataset)
 {
-    OT_ASSERT(aDataset != nullptr);
-
     return AsCoreType(aInstance).Get<MeshCoP::PendingDatasetManager>().Read(AsCoreType(aDataset));
 }
 
 otError otDatasetGetPendingTlvs(otInstance *aInstance, otOperationalDatasetTlvs *aDataset)
 {
-    OT_ASSERT(aDataset != nullptr);
+    AssertPointerIsNotNull(aDataset);
 
     return AsCoreType(aInstance).Get<MeshCoP::PendingDatasetManager>().Read(*aDataset);
 }
 
 otError otDatasetSetPending(otInstance *aInstance, const otOperationalDataset *aDataset)
 {
-    OT_ASSERT(aDataset != nullptr);
-
     return AsCoreType(aInstance).Get<MeshCoP::PendingDatasetManager>().Save(AsCoreType(aDataset));
 }
 
 otError otDatasetSetPendingTlvs(otInstance *aInstance, const otOperationalDatasetTlvs *aDataset)
 {
-    OT_ASSERT(aDataset != nullptr);
+    AssertPointerIsNotNull(aDataset);
 
     return AsCoreType(aInstance).Get<MeshCoP::PendingDatasetManager>().Save(*aDataset);
 }
 
-otError otDatasetSendMgmtActiveGet(otInstance *                          aInstance,
+otError otDatasetSendMgmtActiveGet(otInstance                           *aInstance,
                                    const otOperationalDatasetComponents *aDatasetComponents,
-                                   const uint8_t *                       aTlvTypes,
+                                   const uint8_t                        *aTlvTypes,
                                    uint8_t                               aLength,
-                                   const otIp6Address *                  aAddress)
+                                   const otIp6Address                   *aAddress)
 {
     return AsCoreType(aInstance).Get<MeshCoP::ActiveDatasetManager>().SendGetRequest(AsCoreType(aDatasetComponents),
                                                                                      aTlvTypes, aLength, aAddress);
 }
 
-otError otDatasetSendMgmtActiveSet(otInstance *                aInstance,
+otError otDatasetSendMgmtActiveSet(otInstance                 *aInstance,
                                    const otOperationalDataset *aDataset,
-                                   const uint8_t *             aTlvs,
+                                   const uint8_t              *aTlvs,
                                    uint8_t                     aLength,
                                    otDatasetMgmtSetCallback    aCallback,
-                                   void *                      aContext)
+                                   void                       *aContext)
 {
     return AsCoreType(aInstance).Get<MeshCoP::ActiveDatasetManager>().SendSetRequest(AsCoreType(aDataset), aTlvs,
                                                                                      aLength, aCallback, aContext);
 }
 
-otError otDatasetSendMgmtPendingGet(otInstance *                          aInstance,
+otError otDatasetSendMgmtPendingGet(otInstance                           *aInstance,
                                     const otOperationalDatasetComponents *aDatasetComponents,
-                                    const uint8_t *                       aTlvTypes,
+                                    const uint8_t                        *aTlvTypes,
                                     uint8_t                               aLength,
-                                    const otIp6Address *                  aAddress)
+                                    const otIp6Address                   *aAddress)
 {
     return AsCoreType(aInstance).Get<MeshCoP::PendingDatasetManager>().SendGetRequest(AsCoreType(aDatasetComponents),
                                                                                       aTlvTypes, aLength, aAddress);
 }
 
-otError otDatasetSendMgmtPendingSet(otInstance *                aInstance,
+otError otDatasetSendMgmtPendingSet(otInstance                 *aInstance,
                                     const otOperationalDataset *aDataset,
-                                    const uint8_t *             aTlvs,
+                                    const uint8_t              *aTlvs,
                                     uint8_t                     aLength,
                                     otDatasetMgmtSetCallback    aCallback,
-                                    void *                      aContext)
+                                    void                       *aContext)
 {
     return AsCoreType(aInstance).Get<MeshCoP::PendingDatasetManager>().SendSetRequest(AsCoreType(aDataset), aTlvs,
                                                                                       aLength, aCallback, aContext);
 }
 
 #if OPENTHREAD_FTD
-otError otDatasetGeneratePskc(const char *           aPassPhrase,
-                              const otNetworkName *  aNetworkName,
+otError otDatasetGeneratePskc(const char            *aPassPhrase,
+                              const otNetworkName   *aNetworkName,
                               const otExtendedPanId *aExtPanId,
-                              otPskc *               aPskc)
+                              otPskc                *aPskc)
 {
     return MeshCoP::GeneratePskc(aPassPhrase, AsCoreType(aNetworkName), AsCoreType(aExtPanId), AsCoreType(aPskc));
 }
-#endif // OPENTHREAD_FTD
+#endif
 
 otError otNetworkNameFromString(otNetworkName *aNetworkName, const char *aNameString)
 {
@@ -167,6 +159,8 @@
     Error            error = kErrorNone;
     MeshCoP::Dataset dataset;
 
+    AssertPointerIsNotNull(aDatasetTlvs);
+
     dataset.SetFrom(*aDatasetTlvs);
     VerifyOrExit(dataset.IsValid(), error = kErrorInvalidArgs);
     dataset.ConvertTo(AsCoreType(aDataset));
@@ -174,3 +168,32 @@
 exit:
     return error;
 }
+
+otError otDatasetConvertToTlvs(const otOperationalDataset *aDataset, otOperationalDatasetTlvs *aDatasetTlvs)
+{
+    Error            error = kErrorNone;
+    MeshCoP::Dataset dataset;
+
+    AssertPointerIsNotNull(aDatasetTlvs);
+
+    SuccessOrExit(error = dataset.SetFrom(AsCoreType(aDataset)));
+    dataset.ConvertTo(*aDatasetTlvs);
+
+exit:
+    return error;
+}
+
+otError otDatasetUpdateTlvs(const otOperationalDataset *aDataset, otOperationalDatasetTlvs *aDatasetTlvs)
+{
+    Error            error = kErrorNone;
+    MeshCoP::Dataset dataset;
+
+    AssertPointerIsNotNull(aDatasetTlvs);
+
+    dataset.SetFrom(*aDatasetTlvs);
+    SuccessOrExit(error = dataset.SetFrom(AsCoreType(aDataset)));
+    dataset.ConvertTo(*aDatasetTlvs);
+
+exit:
+    return error;
+}
diff --git a/src/core/api/dataset_updater_api.cpp b/src/core/api/dataset_updater_api.cpp
index 8cd6481..b21577a 100644
--- a/src/core/api/dataset_updater_api.cpp
+++ b/src/core/api/dataset_updater_api.cpp
@@ -43,10 +43,10 @@
 
 using namespace ot;
 
-otError otDatasetUpdaterRequestUpdate(otInstance *                aInstance,
+otError otDatasetUpdaterRequestUpdate(otInstance                 *aInstance,
                                       const otOperationalDataset *aDataset,
                                       otDatasetUpdaterCallback    aCallback,
-                                      void *                      aContext)
+                                      void                       *aContext)
 {
     return AsCoreType(aInstance).Get<MeshCoP::DatasetUpdater>().RequestUpdate(AsCoreType(aDataset), aCallback,
                                                                               aContext);
diff --git a/src/core/api/diags_api.cpp b/src/core/api/diags_api.cpp
index a012d1a..a87bde5 100644
--- a/src/core/api/diags_api.cpp
+++ b/src/core/api/diags_api.cpp
@@ -42,9 +42,11 @@
 
 using namespace ot;
 
-void otDiagProcessCmdLine(otInstance *aInstance, const char *aString, char *aOutput, size_t aOutputMaxLen)
+otError otDiagProcessCmdLine(otInstance *aInstance, const char *aString, char *aOutput, size_t aOutputMaxLen)
 {
-    AsCoreType(aInstance).Get<FactoryDiags::Diags>().ProcessLine(aString, aOutput, aOutputMaxLen);
+    AssertPointerIsNotNull(aString);
+
+    return AsCoreType(aInstance).Get<FactoryDiags::Diags>().ProcessLine(aString, aOutput, aOutputMaxLen);
 }
 
 otError otDiagProcessCmd(otInstance *aInstance, uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen)
@@ -52,9 +54,6 @@
     return AsCoreType(aInstance).Get<FactoryDiags::Diags>().ProcessCmd(aArgsLength, aArgs, aOutput, aOutputMaxLen);
 }
 
-bool otDiagIsEnabled(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<FactoryDiags::Diags>().IsEnabled();
-}
+bool otDiagIsEnabled(otInstance *aInstance) { return AsCoreType(aInstance).Get<FactoryDiags::Diags>().IsEnabled(); }
 
 #endif // OPENTHREAD_CONFIG_DIAG_ENABLE
diff --git a/src/core/api/dns_api.cpp b/src/core/api/dns_api.cpp
index d9bdda7..f1020ec 100644
--- a/src/core/api/dns_api.cpp
+++ b/src/core/api/dns_api.cpp
@@ -51,15 +51,9 @@
 }
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-void otDnsSetNameCompressionEnabled(bool aEnabled)
-{
-    Instance::SetDnsNameCompressionEnabled(aEnabled);
-}
+void otDnsSetNameCompressionEnabled(bool aEnabled) { Instance::SetDnsNameCompressionEnabled(aEnabled); }
 
-bool otDnsIsNameCompressionEnabled(void)
-{
-    return Instance::IsDnsNameCompressionEnabled();
-}
+bool otDnsIsNameCompressionEnabled(void) { return Instance::IsDnsNameCompressionEnabled(); }
 #endif
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
@@ -81,39 +75,45 @@
     }
 }
 
-otError otDnsClientResolveAddress(otInstance *            aInstance,
-                                  const char *            aHostName,
+otError otDnsClientResolveAddress(otInstance             *aInstance,
+                                  const char             *aHostName,
                                   otDnsAddressCallback    aCallback,
-                                  void *                  aContext,
+                                  void                   *aContext,
                                   const otDnsQueryConfig *aConfig)
 {
+    AssertPointerIsNotNull(aHostName);
+
     return AsCoreType(aInstance).Get<Dns::Client>().ResolveAddress(aHostName, aCallback, aContext,
                                                                    AsCoreTypePtr(aConfig));
 }
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
-otError otDnsClientResolveIp4Address(otInstance *            aInstance,
-                                     const char *            aHostName,
+otError otDnsClientResolveIp4Address(otInstance             *aInstance,
+                                     const char             *aHostName,
                                      otDnsAddressCallback    aCallback,
-                                     void *                  aContext,
+                                     void                   *aContext,
                                      const otDnsQueryConfig *aConfig)
 {
+    AssertPointerIsNotNull(aHostName);
+
     return AsCoreType(aInstance).Get<Dns::Client>().ResolveIp4Address(aHostName, aCallback, aContext,
                                                                       AsCoreTypePtr(aConfig));
 }
 #endif
 
 otError otDnsAddressResponseGetHostName(const otDnsAddressResponse *aResponse,
-                                        char *                      aNameBuffer,
+                                        char                       *aNameBuffer,
                                         uint16_t                    aNameBufferSize)
 {
+    AssertPointerIsNotNull(aNameBuffer);
+
     return AsCoreType(aResponse).GetHostName(aNameBuffer, aNameBufferSize);
 }
 
 otError otDnsAddressResponseGetAddress(const otDnsAddressResponse *aResponse,
                                        uint16_t                    aIndex,
-                                       otIp6Address *              aAddress,
-                                       uint32_t *                  aTtl)
+                                       otIp6Address               *aAddress,
+                                       uint32_t                   *aTtl)
 {
     uint32_t ttl;
 
@@ -122,65 +122,81 @@
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
 
-otError otDnsClientBrowse(otInstance *            aInstance,
-                          const char *            aServiceName,
+otError otDnsClientBrowse(otInstance             *aInstance,
+                          const char             *aServiceName,
                           otDnsBrowseCallback     aCallback,
-                          void *                  aContext,
+                          void                   *aContext,
                           const otDnsQueryConfig *aConfig)
 {
+    AssertPointerIsNotNull(aServiceName);
+
     return AsCoreType(aInstance).Get<Dns::Client>().Browse(aServiceName, aCallback, aContext, AsCoreTypePtr(aConfig));
 }
 
 otError otDnsBrowseResponseGetServiceName(const otDnsBrowseResponse *aResponse,
-                                          char *                     aNameBuffer,
+                                          char                      *aNameBuffer,
                                           uint16_t                   aNameBufferSize)
 {
+    AssertPointerIsNotNull(aNameBuffer);
+
     return AsCoreType(aResponse).GetServiceName(aNameBuffer, aNameBufferSize);
 }
 
 otError otDnsBrowseResponseGetServiceInstance(const otDnsBrowseResponse *aResponse,
                                               uint16_t                   aIndex,
-                                              char *                     aLabelBuffer,
+                                              char                      *aLabelBuffer,
                                               uint8_t                    aLabelBufferSize)
 {
+    AssertPointerIsNotNull(aLabelBuffer);
+
     return AsCoreType(aResponse).GetServiceInstance(aIndex, aLabelBuffer, aLabelBufferSize);
 }
 
 otError otDnsBrowseResponseGetServiceInfo(const otDnsBrowseResponse *aResponse,
-                                          const char *               aInstanceLabel,
-                                          otDnsServiceInfo *         aServiceInfo)
+                                          const char                *aInstanceLabel,
+                                          otDnsServiceInfo          *aServiceInfo)
 {
+    AssertPointerIsNotNull(aInstanceLabel);
+
     return AsCoreType(aResponse).GetServiceInfo(aInstanceLabel, AsCoreType(aServiceInfo));
 }
 
 otError otDnsBrowseResponseGetHostAddress(const otDnsBrowseResponse *aResponse,
-                                          const char *               aHostName,
+                                          const char                *aHostName,
                                           uint16_t                   aIndex,
-                                          otIp6Address *             aAddress,
-                                          uint32_t *                 aTtl)
+                                          otIp6Address              *aAddress,
+                                          uint32_t                  *aTtl)
 {
     uint32_t ttl;
 
+    AssertPointerIsNotNull(aHostName);
+
     return AsCoreType(aResponse).GetHostAddress(aHostName, aIndex, AsCoreType(aAddress), aTtl != nullptr ? *aTtl : ttl);
 }
 
-otError otDnsClientResolveService(otInstance *            aInstance,
-                                  const char *            aInstanceLabel,
-                                  const char *            aServiceName,
+otError otDnsClientResolveService(otInstance             *aInstance,
+                                  const char             *aInstanceLabel,
+                                  const char             *aServiceName,
                                   otDnsServiceCallback    aCallback,
-                                  void *                  aContext,
+                                  void                   *aContext,
                                   const otDnsQueryConfig *aConfig)
 {
+    AssertPointerIsNotNull(aInstanceLabel);
+    AssertPointerIsNotNull(aServiceName);
+
     return AsCoreType(aInstance).Get<Dns::Client>().ResolveService(aInstanceLabel, aServiceName, aCallback, aContext,
                                                                    AsCoreTypePtr(aConfig));
 }
 
 otError otDnsServiceResponseGetServiceName(const otDnsServiceResponse *aResponse,
-                                           char *                      aLabelBuffer,
+                                           char                       *aLabelBuffer,
                                            uint8_t                     aLabelBufferSize,
-                                           char *                      aNameBuffer,
+                                           char                       *aNameBuffer,
                                            uint16_t                    aNameBufferSize)
 {
+    AssertPointerIsNotNull(aLabelBuffer);
+    AssertPointerIsNotNull(aNameBuffer);
+
     return AsCoreType(aResponse).GetServiceName(aLabelBuffer, aLabelBufferSize, aNameBuffer, aNameBufferSize);
 }
 
@@ -190,13 +206,15 @@
 }
 
 otError otDnsServiceResponseGetHostAddress(const otDnsServiceResponse *aResponse,
-                                           const char *                aHostName,
+                                           const char                 *aHostName,
                                            uint16_t                    aIndex,
-                                           otIp6Address *              aAddress,
-                                           uint32_t *                  aTtl)
+                                           otIp6Address               *aAddress,
+                                           uint32_t                   *aTtl)
 {
     uint32_t ttl;
 
+    AssertPointerIsNotNull(aHostName);
+
     return AsCoreType(aResponse).GetHostAddress(aHostName, aIndex, AsCoreType(aAddress),
                                                 (aTtl != nullptr) ? *aTtl : ttl);
 }
diff --git a/src/core/api/dns_server_api.cpp b/src/core/api/dns_server_api.cpp
index 5b09448..62e69fc 100644
--- a/src/core/api/dns_server_api.cpp
+++ b/src/core/api/dns_server_api.cpp
@@ -41,20 +41,20 @@
 
 #if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
 
-void otDnssdQuerySetCallbacks(otInstance *                    aInstance,
+void otDnssdQuerySetCallbacks(otInstance                     *aInstance,
                               otDnssdQuerySubscribeCallback   aSubscribe,
                               otDnssdQueryUnsubscribeCallback aUnsubscribe,
-                              void *                          aContext)
+                              void                           *aContext)
 {
     AsCoreType(aInstance).Get<Dns::ServiceDiscovery::Server>().SetQueryCallbacks(aSubscribe, aUnsubscribe, aContext);
 }
 
-void otDnssdQueryHandleDiscoveredServiceInstance(otInstance *                aInstance,
-                                                 const char *                aServiceFullName,
+void otDnssdQueryHandleDiscoveredServiceInstance(otInstance                 *aInstance,
+                                                 const char                 *aServiceFullName,
                                                  otDnssdServiceInstanceInfo *aInstanceInfo)
 {
-    OT_ASSERT(aServiceFullName != nullptr);
-    OT_ASSERT(aInstanceInfo != nullptr);
+    AssertPointerIsNotNull(aServiceFullName);
+    AssertPointerIsNotNull(aInstanceInfo);
 
     AsCoreType(aInstance).Get<Dns::ServiceDiscovery::Server>().HandleDiscoveredServiceInstance(aServiceFullName,
                                                                                                *aInstanceInfo);
@@ -62,8 +62,8 @@
 
 void otDnssdQueryHandleDiscoveredHost(otInstance *aInstance, const char *aHostFullName, otDnssdHostInfo *aHostInfo)
 {
-    OT_ASSERT(aHostFullName != nullptr);
-    OT_ASSERT(aHostInfo != nullptr);
+    AssertPointerIsNotNull(aHostFullName);
+    AssertPointerIsNotNull(aHostInfo);
 
     AsCoreType(aInstance).Get<Dns::ServiceDiscovery::Server>().HandleDiscoveredHost(aHostFullName, *aHostInfo);
 }
@@ -75,8 +75,8 @@
 
 otDnssdQueryType otDnssdGetQueryTypeAndName(const otDnssdQuery *aQuery, char (*aNameOutput)[OT_DNS_MAX_NAME_SIZE])
 {
-    OT_ASSERT(aQuery != nullptr);
-    OT_ASSERT(aNameOutput != nullptr);
+    AssertPointerIsNotNull(aQuery);
+    AssertPointerIsNotNull(aNameOutput);
 
     return MapEnum(Dns::ServiceDiscovery::Server::GetQueryTypeAndName(aQuery, *aNameOutput));
 }
@@ -86,4 +86,16 @@
     return &AsCoreType(aInstance).Get<Dns::ServiceDiscovery::Server>().GetCounters();
 }
 
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+bool otDnssdUpstreamQueryIsEnabled(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<Dns::ServiceDiscovery::Server>().IsUpstreamQueryEnabled();
+}
+
+void otDnssdUpstreamQuerySetEnabled(otInstance *aInstance, bool aEnabled)
+{
+    return AsCoreType(aInstance).Get<Dns::ServiceDiscovery::Server>().SetUpstreamQueryEnabled(aEnabled);
+}
+#endif
+
 #endif // OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
diff --git a/src/core/api/error_api.cpp b/src/core/api/error_api.cpp
index cc2b18d..a884150 100644
--- a/src/core/api/error_api.cpp
+++ b/src/core/api/error_api.cpp
@@ -37,7 +37,4 @@
 
 using namespace ot;
 
-const char *otThreadErrorToString(otError aError)
-{
-    return ErrorToString(aError);
-}
+const char *otThreadErrorToString(otError aError) { return ErrorToString(aError); }
diff --git a/src/core/api/heap_api.cpp b/src/core/api/heap_api.cpp
index e306c61..5946212 100644
--- a/src/core/api/heap_api.cpp
+++ b/src/core/api/heap_api.cpp
@@ -60,13 +60,7 @@
 }
 
 #else  // OPENTHREAD_RADIO
-void *otHeapCAlloc(size_t aCount, size_t aSize)
-{
-    return ot::Heap::CAlloc(aCount, aSize);
-}
+void *otHeapCAlloc(size_t aCount, size_t aSize) { return ot::Heap::CAlloc(aCount, aSize); }
 
-void otHeapFree(void *aPointer)
-{
-    ot::Heap::Free(aPointer);
-}
+void otHeapFree(void *aPointer) { ot::Heap::Free(aPointer); }
 #endif // OPENTHREAD_RADIO
diff --git a/src/core/api/history_tracker_api.cpp b/src/core/api/history_tracker_api.cpp
index 5e52b36..f3d0357 100644
--- a/src/core/api/history_tracker_api.cpp
+++ b/src/core/api/history_tracker_api.cpp
@@ -43,70 +43,92 @@
 
 using namespace ot;
 
-void otHistoryTrackerInitIterator(otHistoryTrackerIterator *aIterator)
-{
-    AsCoreType(aIterator).Init();
-}
+void otHistoryTrackerInitIterator(otHistoryTrackerIterator *aIterator) { AsCoreType(aIterator).Init(); }
 
-const otHistoryTrackerNetworkInfo *otHistoryTrackerIterateNetInfoHistory(otInstance *              aInstance,
+const otHistoryTrackerNetworkInfo *otHistoryTrackerIterateNetInfoHistory(otInstance               *aInstance,
                                                                          otHistoryTrackerIterator *aIterator,
-                                                                         uint32_t *                aEntryAge)
+                                                                         uint32_t                 *aEntryAge)
 {
+    AssertPointerIsNotNull(aEntryAge);
+
     return AsCoreType(aInstance).Get<Utils::HistoryTracker>().IterateNetInfoHistory(AsCoreType(aIterator), *aEntryAge);
 }
 
 const otHistoryTrackerUnicastAddressInfo *otHistoryTrackerIterateUnicastAddressHistory(
-    otInstance *              aInstance,
+    otInstance               *aInstance,
     otHistoryTrackerIterator *aIterator,
-    uint32_t *                aEntryAge)
+    uint32_t                 *aEntryAge)
 {
+    AssertPointerIsNotNull(aEntryAge);
+
     return AsCoreType(aInstance).Get<Utils::HistoryTracker>().IterateUnicastAddressHistory(AsCoreType(aIterator),
                                                                                            *aEntryAge);
 }
 
 const otHistoryTrackerMulticastAddressInfo *otHistoryTrackerIterateMulticastAddressHistory(
-    otInstance *              aInstance,
+    otInstance               *aInstance,
     otHistoryTrackerIterator *aIterator,
-    uint32_t *                aEntryAge)
+    uint32_t                 *aEntryAge)
 {
+    AssertPointerIsNotNull(aEntryAge);
+
     return AsCoreType(aInstance).Get<Utils::HistoryTracker>().IterateMulticastAddressHistory(AsCoreType(aIterator),
                                                                                              *aEntryAge);
 }
 
-const otHistoryTrackerMessageInfo *otHistoryTrackerIterateRxHistory(otInstance *              aInstance,
+const otHistoryTrackerMessageInfo *otHistoryTrackerIterateRxHistory(otInstance               *aInstance,
                                                                     otHistoryTrackerIterator *aIterator,
-                                                                    uint32_t *                aEntryAge)
+                                                                    uint32_t                 *aEntryAge)
 {
+    AssertPointerIsNotNull(aEntryAge);
+
     return AsCoreType(aInstance).Get<Utils::HistoryTracker>().IterateRxHistory(AsCoreType(aIterator), *aEntryAge);
 }
 
-const otHistoryTrackerMessageInfo *otHistoryTrackerIterateTxHistory(otInstance *              aInstance,
+const otHistoryTrackerMessageInfo *otHistoryTrackerIterateTxHistory(otInstance               *aInstance,
                                                                     otHistoryTrackerIterator *aIterator,
-                                                                    uint32_t *                aEntryAge)
+                                                                    uint32_t                 *aEntryAge)
 {
+    AssertPointerIsNotNull(aEntryAge);
+
     return AsCoreType(aInstance).Get<Utils::HistoryTracker>().IterateTxHistory(AsCoreType(aIterator), *aEntryAge);
 }
 
-const otHistoryTrackerNeighborInfo *otHistoryTrackerIterateNeighborHistory(otInstance *              aInstance,
+const otHistoryTrackerNeighborInfo *otHistoryTrackerIterateNeighborHistory(otInstance               *aInstance,
                                                                            otHistoryTrackerIterator *aIterator,
-                                                                           uint32_t *                aEntryAge)
+                                                                           uint32_t                 *aEntryAge)
 {
+    AssertPointerIsNotNull(aEntryAge);
+
     return AsCoreType(aInstance).Get<Utils::HistoryTracker>().IterateNeighborHistory(AsCoreType(aIterator), *aEntryAge);
 }
 
-const otHistoryTrackerOnMeshPrefixInfo *otHistoryTrackerIterateOnMeshPrefixHistory(otInstance *              aInstance,
-                                                                                   otHistoryTrackerIterator *aIterator,
-                                                                                   uint32_t *                aEntryAge)
+const otHistoryTrackerRouterInfo *otHistoryTrackerIterateRouterHistory(otInstance               *aInstance,
+                                                                       otHistoryTrackerIterator *aIterator,
+                                                                       uint32_t                 *aEntryAge)
 {
+    AssertPointerIsNotNull(aEntryAge);
+
+    return AsCoreType(aInstance).Get<Utils::HistoryTracker>().IterateRouterHistory(AsCoreType(aIterator), *aEntryAge);
+}
+
+const otHistoryTrackerOnMeshPrefixInfo *otHistoryTrackerIterateOnMeshPrefixHistory(otInstance               *aInstance,
+                                                                                   otHistoryTrackerIterator *aIterator,
+                                                                                   uint32_t                 *aEntryAge)
+{
+    AssertPointerIsNotNull(aEntryAge);
+
     return AsCoreType(aInstance).Get<Utils::HistoryTracker>().IterateOnMeshPrefixHistory(AsCoreType(aIterator),
                                                                                          *aEntryAge);
 }
 
 const otHistoryTrackerExternalRouteInfo *otHistoryTrackerIterateExternalRouteHistory(
-    otInstance *              aInstance,
+    otInstance               *aInstance,
     otHistoryTrackerIterator *aIterator,
-    uint32_t *                aEntryAge)
+    uint32_t                 *aEntryAge)
 {
+    AssertPointerIsNotNull(aEntryAge);
+
     return AsCoreType(aInstance).Get<Utils::HistoryTracker>().IterateExternalRouteHistory(AsCoreType(aIterator),
                                                                                           *aEntryAge);
 }
diff --git a/src/core/api/icmp6_api.cpp b/src/core/api/icmp6_api.cpp
index 55ed192..c9df55d 100644
--- a/src/core/api/icmp6_api.cpp
+++ b/src/core/api/icmp6_api.cpp
@@ -55,8 +55,8 @@
     return AsCoreType(aInstance).Get<Ip6::Icmp>().RegisterHandler(AsCoreType(aHandler));
 }
 
-otError otIcmp6SendEchoRequest(otInstance *         aInstance,
-                               otMessage *          aMessage,
+otError otIcmp6SendEchoRequest(otInstance          *aInstance,
+                               otMessage           *aMessage,
                                const otMessageInfo *aMessageInfo,
                                uint16_t             aIdentifier)
 {
diff --git a/src/core/api/instance_api.cpp b/src/core/api/instance_api.cpp
index 5557f68..06db73c 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>
@@ -67,10 +67,7 @@
     return instance;
 }
 #else
-otInstance *otInstanceInitSingle(void)
-{
-    return &Instance::InitSingle();
-}
+otInstance *otInstanceInitSingle(void) { return &Instance::InitSingle(); }
 #endif // #if OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE
 
 bool otInstanceIsInitialized(otInstance *aInstance)
@@ -83,24 +80,17 @@
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
 }
 
-void otInstanceFinalize(otInstance *aInstance)
-{
-    AsCoreType(aInstance).Finalize();
-}
+void otInstanceFinalize(otInstance *aInstance) { AsCoreType(aInstance).Finalize(); }
 
-void otInstanceReset(otInstance *aInstance)
-{
-    AsCoreType(aInstance).Reset();
-}
+void otInstanceReset(otInstance *aInstance) { AsCoreType(aInstance).Reset(); }
 
 #if OPENTHREAD_CONFIG_UPTIME_ENABLE
-uint64_t otInstanceGetUptime(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Uptime>().GetUptime();
-}
+uint64_t otInstanceGetUptime(otInstance *aInstance) { return AsCoreType(aInstance).Get<Uptime>().GetUptime(); }
 
 void otInstanceGetUptimeAsString(otInstance *aInstance, char *aBuffer, uint16_t aSize)
 {
+    AssertPointerIsNotNull(aBuffer);
+
     AsCoreType(aInstance).Get<Uptime>().GetUptime(aBuffer, aSize);
 }
 #endif
@@ -116,22 +106,13 @@
     AsCoreType(aInstance).Get<Notifier>().RemoveCallback(aCallback, aContext);
 }
 
-void otInstanceFactoryReset(otInstance *aInstance)
-{
-    AsCoreType(aInstance).FactoryReset();
-}
+void otInstanceFactoryReset(otInstance *aInstance) { AsCoreType(aInstance).FactoryReset(); }
 
-otError otInstanceErasePersistentInfo(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).ErasePersistentInfo();
-}
+otError otInstanceErasePersistentInfo(otInstance *aInstance) { return AsCoreType(aInstance).ErasePersistentInfo(); }
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
 
 #if OPENTHREAD_RADIO
-void otInstanceResetRadioStack(otInstance *aInstance)
-{
-    AsCoreType(aInstance).ResetRadioStack();
-}
+void otInstanceResetRadioStack(otInstance *aInstance) { AsCoreType(aInstance).ResetRadioStack(); }
 #endif
 
 const char *otGetVersionString(void)
@@ -154,7 +135,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 b12b35e..4ff7d22 100644
--- a/src/core/api/ip6_api.cpp
+++ b/src/core/api/ip6_api.cpp
@@ -37,6 +37,9 @@
 
 #include "common/as_core_type.hpp"
 #include "common/locator_getters.hpp"
+#include "net/ip4_types.hpp"
+#include "net/ip6_headers.hpp"
+#include "thread/network_data_leader.hpp"
 #include "utils/slaac_address.hpp"
 
 using namespace ot;
@@ -65,10 +68,7 @@
     return error;
 }
 
-bool otIp6IsEnabled(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<ThreadNetif>().IsUp();
-}
+bool otIp6IsEnabled(otInstance *aInstance) { return AsCoreType(aInstance).Get<ThreadNetif>().IsUp(); }
 
 const otNetifAddress *otIp6GetUnicastAddresses(otInstance *aInstance)
 {
@@ -133,7 +133,7 @@
 otError otIp6Send(otInstance *aInstance, otMessage *aMessage)
 {
     return AsCoreType(aInstance).Get<Ip6::Ip6>().SendRaw(AsCoreType(aMessage),
-                                                         !OPENTHREAD_CONFIG_IP6_ALLOW_LOOP_BACK_HOST_DATAGRAMS);
+                                                         OPENTHREAD_CONFIG_IP6_ALLOW_LOOP_BACK_HOST_DATAGRAMS);
 }
 
 otMessage *otIp6NewMessage(otInstance *aInstance, const otMessageSettings *aSettings)
@@ -141,14 +141,13 @@
     return AsCoreType(aInstance).Get<Ip6::Ip6>().NewMessage(0, Message::Settings::From(aSettings));
 }
 
-otMessage *otIp6NewMessageFromBuffer(otInstance *             aInstance,
-                                     const uint8_t *          aData,
+otMessage *otIp6NewMessageFromBuffer(otInstance              *aInstance,
+                                     const uint8_t           *aData,
                                      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)
@@ -168,6 +167,8 @@
 
 const uint16_t *otIp6GetUnsecurePorts(otInstance *aInstance, uint8_t *aNumEntries)
 {
+    AssertPointerIsNotNull(aNumEntries);
+
     return AsCoreType(aInstance).Get<Ip6::Filter>().GetUnsecurePorts(*aNumEntries);
 }
 
@@ -186,53 +187,56 @@
     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);
+
     AsCoreType(aAddress).ToString(aBuffer, aSize);
 }
 
 void otIp6SockAddrToString(const otSockAddr *aSockAddr, char *aBuffer, uint16_t aSize)
 {
+    AssertPointerIsNotNull(aBuffer);
+
     AsCoreType(aSockAddr).ToString(aBuffer, aSize);
 }
 
 void otIp6PrefixToString(const otIp6Prefix *aPrefix, char *aBuffer, uint16_t aSize)
 {
+    AssertPointerIsNotNull(aBuffer);
+
     AsCoreType(aPrefix).ToString(aBuffer, aSize);
 }
 
 uint8_t otIp6PrefixMatch(const otIp6Address *aFirst, const otIp6Address *aSecond)
 {
-    OT_ASSERT(aFirst != nullptr && aSecond != nullptr);
-
     return AsCoreType(aFirst).PrefixMatch(AsCoreType(aSecond));
 }
 
-bool otIp6IsAddressUnspecified(const otIp6Address *aAddress)
+void otIp6GetPrefix(const otIp6Address *aAddress, uint8_t aLength, otIp6Prefix *aPrefix)
 {
-    return AsCoreType(aAddress).IsUnspecified();
+    AsCoreType(aAddress).GetPrefix(aLength, AsCoreType(aPrefix));
 }
 
+bool otIp6IsAddressUnspecified(const otIp6Address *aAddress) { return AsCoreType(aAddress).IsUnspecified(); }
+
 otError otIp6SelectSourceAddress(otInstance *aInstance, otMessageInfo *aMessageInfo)
 {
-    Error                             error = kErrorNone;
-    const Ip6::Netif::UnicastAddress *netifAddr;
-
-    netifAddr = AsCoreType(aInstance).Get<Ip6::Ip6>().SelectSourceAddress(AsCoreType(aMessageInfo));
-    VerifyOrExit(netifAddr != nullptr, error = kErrorNotFound);
-    aMessageInfo->mSockAddr = netifAddr->GetAddress();
-
-exit:
-    return error;
+    return AsCoreType(aInstance).Get<Ip6::Ip6>().SelectSourceAddress(AsCoreType(aMessageInfo));
 }
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE
-otError otIp6RegisterMulticastListeners(otInstance *                            aInstance,
-                                        const otIp6Address *                    aAddresses,
+otError otIp6RegisterMulticastListeners(otInstance                             *aInstance,
+                                        const otIp6Address                     *aAddresses,
                                         uint8_t                                 aAddressNum,
-                                        const uint32_t *                        aTimeout,
+                                        const uint32_t                         *aTimeout,
                                         otIp6RegisterMulticastListenersCallback aCallback,
-                                        void *                                  aContext)
+                                        void                                   *aContext)
 {
     return AsCoreType(aInstance).Get<MlrManager>().RegisterMulticastListeners(aAddresses, aAddressNum, aTimeout,
                                                                               aCallback, aContext);
@@ -241,10 +245,7 @@
 
 #if OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE
 
-bool otIp6IsSlaacEnabled(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Utils::Slaac>().IsEnabled();
-}
+bool otIp6IsSlaacEnabled(otInstance *aInstance) { return AsCoreType(aInstance).Get<Utils::Slaac>().IsEnabled(); }
 
 void otIp6SetSlaacEnabled(otInstance *aInstance, bool aEnabled)
 {
@@ -276,7 +277,16 @@
 
 #endif
 
-const char *otIp6ProtoToString(uint8_t aIpProto)
+const char *otIp6ProtoToString(uint8_t aIpProto) { return Ip6::Ip6::IpProtoToString(aIpProto); }
+
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+const otBorderRoutingCounters *otIp6GetBorderRoutingCounters(otInstance *aInstance)
 {
-    return Ip6::Ip6::IpProtoToString(aIpProto);
+    return &AsCoreType(aInstance).Get<Ip6::Ip6>().GetBorderRoutingCounters();
 }
+
+void otIp6ResetBorderRoutingCounters(otInstance *aInstance)
+{
+    AsCoreType(aInstance).Get<Ip6::Ip6>().ResetBorderRoutingCounters();
+}
+#endif
diff --git a/src/core/api/jam_detection_api.cpp b/src/core/api/jam_detection_api.cpp
index d108bdd..277f64c 100644
--- a/src/core/api/jam_detection_api.cpp
+++ b/src/core/api/jam_detection_api.cpp
@@ -79,10 +79,7 @@
     return AsCoreType(aInstance).Get<Utils::JamDetector>().Start(aCallback, aContext);
 }
 
-otError otJamDetectionStop(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Utils::JamDetector>().Stop();
-}
+otError otJamDetectionStop(otInstance *aInstance) { return AsCoreType(aInstance).Get<Utils::JamDetector>().Stop(); }
 
 bool otJamDetectionIsEnabled(otInstance *aInstance)
 {
diff --git a/src/core/api/joiner_api.cpp b/src/core/api/joiner_api.cpp
index 2ee2ac3..ceee394 100644
--- a/src/core/api/joiner_api.cpp
+++ b/src/core/api/joiner_api.cpp
@@ -43,24 +43,21 @@
 
 using namespace ot;
 
-otError otJoinerStart(otInstance *     aInstance,
-                      const char *     aPskd,
-                      const char *     aProvisioningUrl,
-                      const char *     aVendorName,
-                      const char *     aVendorModel,
-                      const char *     aVendorSwVersion,
-                      const char *     aVendorData,
+otError otJoinerStart(otInstance      *aInstance,
+                      const char      *aPskd,
+                      const char      *aProvisioningUrl,
+                      const char      *aVendorName,
+                      const char      *aVendorModel,
+                      const char      *aVendorSwVersion,
+                      const char      *aVendorData,
                       otJoinerCallback aCallback,
-                      void *           aContext)
+                      void            *aContext)
 {
     return AsCoreType(aInstance).Get<MeshCoP::Joiner>().Start(aPskd, aProvisioningUrl, aVendorName, aVendorModel,
                                                               aVendorSwVersion, aVendorData, aCallback, aContext);
 }
 
-void otJoinerStop(otInstance *aInstance)
-{
-    AsCoreType(aInstance).Get<MeshCoP::Joiner>().Stop();
-}
+void otJoinerStop(otInstance *aInstance) { AsCoreType(aInstance).Get<MeshCoP::Joiner>().Stop(); }
 
 otJoinerState otJoinerGetState(otInstance *aInstance)
 {
diff --git a/src/core/api/link_api.cpp b/src/core/api/link_api.cpp
index ff5922d..56ca549 100644
--- a/src/core/api/link_api.cpp
+++ b/src/core/api/link_api.cpp
@@ -112,7 +112,6 @@
     Error     error    = kErrorNone;
     Instance &instance = AsCoreType(aInstance);
 
-    OT_ASSERT(aExtAddress != nullptr);
     VerifyOrExit(instance.Get<Mle::MleRouter>().IsDisabled(), error = kErrorInvalidState);
 
     instance.Get<Mac::Mac>().SetExtAddress(AsCoreType(aExtAddress));
@@ -128,10 +127,7 @@
     AsCoreType(aInstance).Get<Radio>().GetIeeeEui64(AsCoreType(aEui64));
 }
 
-otPanId otLinkGetPanId(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mac::Mac>().GetPanId();
-}
+otPanId otLinkGetPanId(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mac::Mac>().GetPanId(); }
 
 otError otLinkSetPanId(otInstance *aInstance, otPanId aPanId)
 {
@@ -206,15 +202,11 @@
 
 otError otLinkFilterAddAddress(otInstance *aInstance, const otExtAddress *aExtAddress)
 {
-    OT_ASSERT(aExtAddress != nullptr);
-
     return AsCoreType(aInstance).Get<Mac::Filter>().AddAddress(AsCoreType(aExtAddress));
 }
 
 void otLinkFilterRemoveAddress(otInstance *aInstance, const otExtAddress *aExtAddress)
 {
-    OT_ASSERT(aExtAddress != nullptr);
-
     AsCoreType(aInstance).Get<Mac::Filter>().RemoveAddress(AsCoreType(aExtAddress));
 }
 
@@ -225,22 +217,19 @@
 
 otError otLinkFilterGetNextAddress(otInstance *aInstance, otMacFilterIterator *aIterator, otMacFilterEntry *aEntry)
 {
-    OT_ASSERT(aIterator != nullptr && aEntry != nullptr);
+    AssertPointerIsNotNull(aIterator);
+    AssertPointerIsNotNull(aEntry);
 
     return AsCoreType(aInstance).Get<Mac::Filter>().GetNextAddress(*aIterator, *aEntry);
 }
 
 otError otLinkFilterAddRssIn(otInstance *aInstance, const otExtAddress *aExtAddress, int8_t aRss)
 {
-    OT_ASSERT(aExtAddress != nullptr);
-
     return AsCoreType(aInstance).Get<Mac::Filter>().AddRssIn(AsCoreType(aExtAddress), aRss);
 }
 
 void otLinkFilterRemoveRssIn(otInstance *aInstance, const otExtAddress *aExtAddress)
 {
-    OT_ASSERT(aExtAddress != nullptr);
-
     AsCoreType(aInstance).Get<Mac::Filter>().RemoveRssIn(AsCoreType(aExtAddress));
 }
 
@@ -254,14 +243,12 @@
     AsCoreType(aInstance).Get<Mac::Filter>().ClearDefaultRssIn();
 }
 
-void otLinkFilterClearAllRssIn(otInstance *aInstance)
-{
-    AsCoreType(aInstance).Get<Mac::Filter>().ClearAllRssIn();
-}
+void otLinkFilterClearAllRssIn(otInstance *aInstance) { AsCoreType(aInstance).Get<Mac::Filter>().ClearAllRssIn(); }
 
 otError otLinkFilterGetNextRssIn(otInstance *aInstance, otMacFilterIterator *aIterator, otMacFilterEntry *aEntry)
 {
-    OT_ASSERT(aIterator != nullptr && aEntry != nullptr);
+    AssertPointerIsNotNull(aIterator);
+    AssertPointerIsNotNull(aEntry);
 
     return AsCoreType(aInstance).Get<Mac::Filter>().GetNextRssIn(*aIterator, *aEntry);
 }
@@ -282,18 +269,20 @@
 
 uint8_t otLinkConvertRssToLinkQuality(otInstance *aInstance, int8_t aRss)
 {
-    return LinkQualityInfo::ConvertRssToLinkQuality(AsCoreType(aInstance).Get<Mac::Mac>().GetNoiseFloor(), aRss);
+    return LinkQualityForLinkMargin(AsCoreType(aInstance).Get<Mac::Mac>().ComputeLinkMargin(aRss));
 }
 
 int8_t otLinkConvertLinkQualityToRss(otInstance *aInstance, uint8_t aLinkQuality)
 {
-    return LinkQualityInfo::ConvertLinkQualityToRss(AsCoreType(aInstance).Get<Mac::Mac>().GetNoiseFloor(),
-                                                    static_cast<LinkQuality>(aLinkQuality));
+    return GetTypicalRssForLinkQuality(AsCoreType(aInstance).Get<Mac::Mac>().GetNoiseFloor(),
+                                       static_cast<LinkQuality>(aLinkQuality));
 }
 
 #if OPENTHREAD_CONFIG_MAC_RETRY_SUCCESS_HISTOGRAM_ENABLE
 const uint32_t *otLinkGetTxDirectRetrySuccessHistogram(otInstance *aInstance, uint8_t *aNumberOfEntries)
 {
+    AssertPointerIsNotNull(aNumberOfEntries);
+
     return AsCoreType(aInstance).Get<Mac::Mac>().GetDirectRetrySuccessHistogram(*aNumberOfEntries);
 }
 
@@ -301,6 +290,8 @@
 {
     const uint32_t *histogram = nullptr;
 
+    AssertPointerIsNotNull(aNumberOfEntries);
+
 #if OPENTHREAD_FTD
     histogram = AsCoreType(aInstance).Get<Mac::Mac>().GetIndirectRetrySuccessHistogram(*aNumberOfEntries);
 #else
@@ -322,10 +313,7 @@
     AsCoreType(aInstance).Get<Mac::Mac>().SetPcapCallback(aPcapCallback, aCallbackContext);
 }
 
-bool otLinkIsPromiscuous(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mac::Mac>().IsPromiscuous();
-}
+bool otLinkIsPromiscuous(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mac::Mac>().IsPromiscuous(); }
 
 otError otLinkSetPromiscuous(otInstance *aInstance, bool aPromiscuous)
 {
@@ -355,26 +343,20 @@
     return error;
 }
 
-bool otLinkIsEnabled(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mac::Mac>().IsEnabled();
-}
+bool otLinkIsEnabled(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mac::Mac>().IsEnabled(); }
 
 const otMacCounters *otLinkGetCounters(otInstance *aInstance)
 {
     return &AsCoreType(aInstance).Get<Mac::Mac>().GetCounters();
 }
 
-void otLinkResetCounters(otInstance *aInstance)
-{
-    AsCoreType(aInstance).Get<Mac::Mac>().ResetCounters();
-}
+void otLinkResetCounters(otInstance *aInstance) { AsCoreType(aInstance).Get<Mac::Mac>().ResetCounters(); }
 
-otError otLinkActiveScan(otInstance *             aInstance,
+otError otLinkActiveScan(otInstance              *aInstance,
                          uint32_t                 aScanChannels,
                          uint16_t                 aScanDuration,
                          otHandleActiveScanResult aCallback,
-                         void *                   aCallbackContext)
+                         void                    *aCallbackContext)
 {
     return AsCoreType(aInstance).Get<Mac::Mac>().ActiveScan(aScanChannels, aScanDuration, aCallback, aCallbackContext);
 }
@@ -384,11 +366,11 @@
     return AsCoreType(aInstance).Get<Mac::Mac>().IsActiveScanInProgress();
 }
 
-otError otLinkEnergyScan(otInstance *             aInstance,
+otError otLinkEnergyScan(otInstance              *aInstance,
                          uint32_t                 aScanChannels,
                          uint16_t                 aScanDuration,
                          otHandleEnergyScanResult aCallback,
-                         void *                   aCallbackContext)
+                         void                    *aCallbackContext)
 {
     return AsCoreType(aInstance).Get<Mac::Mac>().EnergyScan(aScanChannels, aScanDuration, aCallback, aCallbackContext);
 }
@@ -409,10 +391,7 @@
 }
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-uint8_t otLinkCslGetChannel(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mac::Mac>().GetCslChannel();
-}
+uint8_t otLinkCslGetChannel(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mac::Mac>().GetCslChannel(); }
 
 otError otLinkCslSetChannel(otInstance *aInstance, uint8_t aChannel)
 {
@@ -426,10 +405,7 @@
     return error;
 }
 
-uint16_t otLinkCslGetPeriod(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mac::Mac>().GetCslPeriod();
-}
+uint16_t otLinkCslGetPeriod(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mac::Mac>().GetCslPeriod(); }
 
 otError otLinkCslSetPeriod(otInstance *aInstance, uint16_t aPeriod)
 {
diff --git a/src/core/api/link_metrics_api.cpp b/src/core/api/link_metrics_api.cpp
index b14a915..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"
@@ -42,70 +41,63 @@
 
 using namespace ot;
 
-otError otLinkMetricsQuery(otInstance *                aInstance,
-                           const otIp6Address *        aDestination,
+otError otLinkMetricsQuery(otInstance                 *aInstance,
+                           const otIp6Address         *aDestination,
                            uint8_t                     aSeriesId,
-                           const otLinkMetrics *       aLinkMetricsFlags,
+                           const otLinkMetrics        *aLinkMetricsFlags,
                            otLinkMetricsReportCallback aCallback,
-                           void *                      aCallbackContext)
+                           void                       *aCallbackContext)
 {
-    OT_ASSERT(aDestination != nullptr);
+    AsCoreType(aInstance).Get<LinkMetrics::Initiator>().SetReportCallback(aCallback, aCallbackContext);
 
-    AsCoreType(aInstance).Get<LinkMetrics::LinkMetrics>().SetReportCallback(aCallback, aCallbackContext);
-
-    return AsCoreType(aInstance).Get<LinkMetrics::LinkMetrics>().Query(AsCoreType(aDestination), aSeriesId,
-                                                                       AsCoreTypePtr(aLinkMetricsFlags));
-}
-
-otError otLinkMetricsConfigForwardTrackingSeries(otInstance *                      aInstance,
-                                                 const otIp6Address *              aDestination,
-                                                 uint8_t                           aSeriesId,
-                                                 const otLinkMetricsSeriesFlags    aSeriesFlags,
-                                                 const otLinkMetrics *             aLinkMetricsFlags,
-                                                 otLinkMetricsMgmtResponseCallback aCallback,
-                                                 void *                            aCallbackContext)
-{
-    OT_ASSERT(aDestination != nullptr);
-
-    LinkMetrics::LinkMetrics &linkMetrics = AsCoreType(aInstance).Get<LinkMetrics::LinkMetrics>();
-
-    linkMetrics.SetMgmtResponseCallback(aCallback, aCallbackContext);
-
-    return linkMetrics.SendMgmtRequestForwardTrackingSeries(AsCoreType(aDestination), aSeriesId, aSeriesFlags,
-                                                            AsCoreTypePtr(aLinkMetricsFlags));
+    return AsCoreType(aInstance).Get<LinkMetrics::Initiator>().Query(AsCoreType(aDestination), aSeriesId,
+                                                                     AsCoreTypePtr(aLinkMetricsFlags));
 }
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
-otError otLinkMetricsConfigEnhAckProbing(otInstance *                               aInstance,
-                                         const otIp6Address *                       aDestination,
-                                         otLinkMetricsEnhAckFlags                   aEnhAckFlags,
-                                         const otLinkMetrics *                      aLinkMetricsFlags,
-                                         otLinkMetricsMgmtResponseCallback          aCallback,
-                                         void *                                     aCallbackContext,
-                                         otLinkMetricsEnhAckProbingIeReportCallback aEnhAckCallback,
-                                         void *                                     aEnhAckCallbackContext)
+otError otLinkMetricsConfigForwardTrackingSeries(otInstance                       *aInstance,
+                                                 const otIp6Address               *aDestination,
+                                                 uint8_t                           aSeriesId,
+                                                 const otLinkMetricsSeriesFlags    aSeriesFlags,
+                                                 const otLinkMetrics              *aLinkMetricsFlags,
+                                                 otLinkMetricsMgmtResponseCallback aCallback,
+                                                 void                             *aCallbackContext)
 {
-    OT_ASSERT(aDestination != nullptr);
+    LinkMetrics::Initiator &initiator = AsCoreType(aInstance).Get<LinkMetrics::Initiator>();
 
-    LinkMetrics::LinkMetrics &linkMetrics = AsCoreType(aInstance).Get<LinkMetrics::LinkMetrics>();
+    initiator.SetMgmtResponseCallback(aCallback, aCallbackContext);
 
-    linkMetrics.SetMgmtResponseCallback(aCallback, aCallbackContext);
-    linkMetrics.SetEnhAckProbingCallback(aEnhAckCallback, aEnhAckCallbackContext);
-
-    return linkMetrics.SendMgmtRequestEnhAckProbing(AsCoreType(aDestination), MapEnum(aEnhAckFlags),
-                                                    AsCoreTypePtr(aLinkMetricsFlags));
+    return initiator.SendMgmtRequestForwardTrackingSeries(AsCoreType(aDestination), aSeriesId,
+                                                          AsCoreType(&aSeriesFlags), AsCoreTypePtr(aLinkMetricsFlags));
 }
 
-otError otLinkMetricsSendLinkProbe(otInstance *        aInstance,
+otError otLinkMetricsConfigEnhAckProbing(otInstance                                *aInstance,
+                                         const otIp6Address                        *aDestination,
+                                         otLinkMetricsEnhAckFlags                   aEnhAckFlags,
+                                         const otLinkMetrics                       *aLinkMetricsFlags,
+                                         otLinkMetricsMgmtResponseCallback          aCallback,
+                                         void                                      *aCallbackContext,
+                                         otLinkMetricsEnhAckProbingIeReportCallback aEnhAckCallback,
+                                         void                                      *aEnhAckCallbackContext)
+{
+    LinkMetrics::Initiator &initiator = AsCoreType(aInstance).Get<LinkMetrics::Initiator>();
+
+    initiator.SetMgmtResponseCallback(aCallback, aCallbackContext);
+    initiator.SetEnhAckProbingCallback(aEnhAckCallback, aEnhAckCallbackContext);
+
+    return initiator.SendMgmtRequestEnhAckProbing(AsCoreType(aDestination), MapEnum(aEnhAckFlags),
+                                                  AsCoreTypePtr(aLinkMetricsFlags));
+}
+
+otError otLinkMetricsSendLinkProbe(otInstance         *aInstance,
                                    const otIp6Address *aDestination,
                                    uint8_t             aSeriesId,
                                    uint8_t             aLength)
 {
-    OT_ASSERT(aDestination != nullptr);
-    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/link_raw_api.cpp b/src/core/api/link_raw_api.cpp
index fafe69c..12a76d5 100644
--- a/src/core/api/link_raw_api.cpp
+++ b/src/core/api/link_raw_api.cpp
@@ -54,20 +54,14 @@
     return AsCoreType(aInstance).Get<Mac::LinkRaw>().SetReceiveDone(aCallback);
 }
 
-bool otLinkRawIsEnabled(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mac::LinkRaw>().IsEnabled();
-}
+bool otLinkRawIsEnabled(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mac::LinkRaw>().IsEnabled(); }
 
 otError otLinkRawSetShortAddress(otInstance *aInstance, uint16_t aShortAddress)
 {
     return AsCoreType(aInstance).Get<Mac::LinkRaw>().SetShortAddress(aShortAddress);
 }
 
-bool otLinkRawGetPromiscuous(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Radio>().GetPromiscuous();
-}
+bool otLinkRawGetPromiscuous(otInstance *aInstance) { return AsCoreType(aInstance).Get<Radio>().GetPromiscuous(); }
 
 otError otLinkRawSetPromiscuous(otInstance *aInstance, bool aEnable)
 {
@@ -94,10 +88,7 @@
     return error;
 }
 
-otError otLinkRawReceive(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mac::LinkRaw>().Receive();
-}
+otError otLinkRawReceive(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mac::LinkRaw>().Receive(); }
 
 otRadioFrame *otLinkRawGetTransmitBuffer(otInstance *aInstance)
 {
@@ -109,17 +100,11 @@
     return AsCoreType(aInstance).Get<Mac::LinkRaw>().Transmit(aCallback);
 }
 
-int8_t otLinkRawGetRssi(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Radio>().GetRssi();
-}
+int8_t otLinkRawGetRssi(otInstance *aInstance) { return AsCoreType(aInstance).Get<Radio>().GetRssi(); }
 
-otRadioCaps otLinkRawGetCaps(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mac::LinkRaw>().GetCaps();
-}
+otRadioCaps otLinkRawGetCaps(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mac::LinkRaw>().GetCaps(); }
 
-otError otLinkRawEnergyScan(otInstance *            aInstance,
+otError otLinkRawEnergyScan(otInstance             *aInstance,
                             uint8_t                 aScanChannel,
                             uint16_t                aScanDuration,
                             otLinkRawEnergyScanDone aCallback)
@@ -157,7 +142,9 @@
 {
     Mac::ExtAddress address;
     Error           error    = kErrorNone;
-    Instance &      instance = AsCoreType(aInstance);
+    Instance       &instance = AsCoreType(aInstance);
+
+    AssertPointerIsNotNull(aExtAddress);
 
     VerifyOrExit(instance.Get<Mac::LinkRaw>().IsEnabled(), error = kErrorInvalidState);
 
@@ -184,7 +171,9 @@
 {
     Mac::ExtAddress address;
     Error           error    = kErrorNone;
-    Instance &      instance = AsCoreType(aInstance);
+    Instance       &instance = AsCoreType(aInstance);
+
+    AssertPointerIsNotNull(aExtAddress);
 
     VerifyOrExit(instance.Get<Mac::LinkRaw>().IsEnabled(), error = kErrorInvalidState);
 
@@ -221,7 +210,7 @@
     return error;
 }
 
-otError otLinkRawSetMacKey(otInstance *    aInstance,
+otError otLinkRawSetMacKey(otInstance     *aInstance,
                            uint8_t         aKeyIdMode,
                            uint8_t         aKeyId,
                            const otMacKey *aPrevKey,
@@ -234,7 +223,12 @@
 
 otError otLinkRawSetMacFrameCounter(otInstance *aInstance, uint32_t aMacFrameCounter)
 {
-    return AsCoreType(aInstance).Get<Mac::LinkRaw>().SetMacFrameCounter(aMacFrameCounter);
+    return AsCoreType(aInstance).Get<Mac::LinkRaw>().SetMacFrameCounter(aMacFrameCounter, /* aSetIfLarger */ false);
+}
+
+otError otLinkRawSetMacFrameCounterIfLarger(otInstance *aInstance, uint32_t aMacFrameCounter)
+{
+    return AsCoreType(aInstance).Get<Mac::LinkRaw>().SetMacFrameCounter(aMacFrameCounter, /* aSetIfLarger */ true);
 }
 
 uint64_t otLinkRawGetRadioTime(otInstance *aInstance)
@@ -251,20 +245,14 @@
     return OT_DEVICE_ROLE_DISABLED;
 }
 
-uint8_t otLinkGetChannel(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mac::LinkRaw>().GetChannel();
-}
+uint8_t otLinkGetChannel(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mac::LinkRaw>().GetChannel(); }
 
 otError otLinkSetChannel(otInstance *aInstance, uint8_t aChannel)
 {
     return AsCoreType(aInstance).Get<Mac::LinkRaw>().SetChannel(aChannel);
 }
 
-otPanId otLinkGetPanId(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mac::LinkRaw>().GetPanId();
-}
+otPanId otLinkGetPanId(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mac::LinkRaw>().GetPanId(); }
 
 otError otLinkSetPanId(otInstance *aInstance, uint16_t aPanId)
 {
@@ -288,6 +276,8 @@
 
 void otLinkGetFactoryAssignedIeeeEui64(otInstance *aInstance, otExtAddress *aEui64)
 {
+    AssertPointerIsNotNull(aEui64);
+
     otPlatRadioGetIeeeEui64(aInstance, aEui64->m8);
 }
 
diff --git a/src/core/api/logging_api.cpp b/src/core/api/logging_api.cpp
index 3218837..edce6b9 100644
--- a/src/core/api/logging_api.cpp
+++ b/src/core/api/logging_api.cpp
@@ -41,10 +41,7 @@
 
 using namespace ot;
 
-otLogLevel otLoggingGetLevel(void)
-{
-    return static_cast<otLogLevel>(Instance::GetLogLevel());
-}
+otLogLevel otLoggingGetLevel(void) { return static_cast<otLogLevel>(Instance::GetLogLevel()); }
 
 #if OPENTHREAD_CONFIG_LOG_LEVEL_DYNAMIC_ENABLE
 otError otLoggingSetLevel(otLogLevel aLogLevel)
diff --git a/src/core/api/mesh_diag_api.cpp b/src/core/api/mesh_diag_api.cpp
new file mode 100644
index 0000000..888476a
--- /dev/null
+++ b/src/core/api/mesh_diag_api.cpp
@@ -0,0 +1,67 @@
+/*
+ *  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 the Mesh Diagnostics public APIs.
+ */
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
+
+#include <openthread/mesh_diag.h>
+
+#include "common/as_core_type.hpp"
+#include "common/locator_getters.hpp"
+#include "utils/mesh_diag.hpp"
+
+using namespace ot;
+
+otError otMeshDiagDiscoverTopology(otInstance                     *aInstance,
+                                   const otMeshDiagDiscoverConfig *aConfig,
+                                   otMeshDiagDiscoverCallback      aCallback,
+                                   void                           *aContext)
+{
+    AssertPointerIsNotNull(aConfig);
+    return AsCoreType(aInstance).Get<Utils::MeshDiag>().DiscoverTopology(*aConfig, aCallback, aContext);
+}
+
+void otMeshDiagCancel(otInstance *aInstance) { AsCoreType(aInstance).Get<Utils::MeshDiag>().Cancel(); }
+
+otError otMeshDiagGetNextIp6Address(otMeshDiagIp6AddrIterator *aIterator, otIp6Address *aIp6Address)
+{
+    return AsCoreType(aIterator).GetNextAddress(AsCoreType(aIp6Address));
+}
+
+otError otMeshDiagGetNextChildInfo(otMeshDiagChildIterator *aIterator, otMeshDiagChildInfo *aChildInfo)
+{
+    return AsCoreType(aIterator).GetNextChildInfo(AsCoreType(aChildInfo));
+}
+
+#endif // OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
diff --git a/src/core/api/message_api.cpp b/src/core/api/message_api.cpp
index ecc3bfc..eafc1d2 100644
--- a/src/core/api/message_api.cpp
+++ b/src/core/api/message_api.cpp
@@ -40,35 +40,17 @@
 
 using namespace ot;
 
-void otMessageFree(otMessage *aMessage)
-{
-    AsCoreType(aMessage).Free();
-}
+void otMessageFree(otMessage *aMessage) { AsCoreType(aMessage).Free(); }
 
-uint16_t otMessageGetLength(const otMessage *aMessage)
-{
-    return AsCoreType(aMessage).GetLength();
-}
+uint16_t otMessageGetLength(const otMessage *aMessage) { return AsCoreType(aMessage).GetLength(); }
 
-otError otMessageSetLength(otMessage *aMessage, uint16_t aLength)
-{
-    return AsCoreType(aMessage).SetLength(aLength);
-}
+otError otMessageSetLength(otMessage *aMessage, uint16_t aLength) { return AsCoreType(aMessage).SetLength(aLength); }
 
-uint16_t otMessageGetOffset(const otMessage *aMessage)
-{
-    return AsCoreType(aMessage).GetOffset();
-}
+uint16_t otMessageGetOffset(const otMessage *aMessage) { return AsCoreType(aMessage).GetOffset(); }
 
-void otMessageSetOffset(otMessage *aMessage, uint16_t aOffset)
-{
-    AsCoreType(aMessage).SetOffset(aOffset);
-}
+void otMessageSetOffset(otMessage *aMessage, uint16_t aOffset) { AsCoreType(aMessage).SetOffset(aOffset); }
 
-bool otMessageIsLinkSecurityEnabled(const otMessage *aMessage)
-{
-    return AsCoreType(aMessage).IsLinkSecurityEnabled();
-}
+bool otMessageIsLinkSecurityEnabled(const otMessage *aMessage) { return AsCoreType(aMessage).IsLinkSecurityEnabled(); }
 
 void otMessageSetDirectTransmission(otMessage *aMessage, bool aEnabled)
 {
@@ -82,23 +64,26 @@
     }
 }
 
-int8_t otMessageGetRss(const otMessage *aMessage)
-{
-    return AsCoreType(aMessage).GetAverageRss();
-}
+int8_t otMessageGetRss(const otMessage *aMessage) { return AsCoreType(aMessage).GetAverageRss(); }
 
 otError otMessageAppend(otMessage *aMessage, const void *aBuf, uint16_t aLength)
 {
+    AssertPointerIsNotNull(aBuf);
+
     return AsCoreType(aMessage).AppendBytes(aBuf, aLength);
 }
 
 uint16_t otMessageRead(const otMessage *aMessage, uint16_t aOffset, void *aBuf, uint16_t aLength)
 {
+    AssertPointerIsNotNull(aBuf);
+
     return AsCoreType(aMessage).ReadBytes(aOffset, aBuf, aLength);
 }
 
 int otMessageWrite(otMessage *aMessage, uint16_t aOffset, const void *aBuf, uint16_t aLength)
 {
+    AssertPointerIsNotNull(aBuf);
+
     AsCoreType(aMessage).WriteBytes(aOffset, aBuf, aLength);
 
     return aLength;
@@ -106,6 +91,8 @@
 
 void otMessageQueueInit(otMessageQueue *aQueue)
 {
+    AssertPointerIsNotNull(aQueue);
+
     aQueue->mData = nullptr;
 }
 
@@ -124,10 +111,7 @@
     AsCoreType(aQueue).Dequeue(AsCoreType(aMessage));
 }
 
-otMessage *otMessageQueueGetHead(otMessageQueue *aQueue)
-{
-    return AsCoreType(aQueue).GetHead();
-}
+otMessage *otMessageQueueGetHead(otMessageQueue *aQueue) { return AsCoreType(aQueue).GetHead(); }
 
 otMessage *otMessageQueueGetNext(otMessageQueue *aQueue, const otMessage *aMessage)
 {
@@ -147,4 +131,6 @@
 {
     AsCoreType(aInstance).GetBufferInfo(AsCoreType(aBufferInfo));
 }
+
+void otMessageResetBufferInfo(otInstance *aInstance) { AsCoreType(aInstance).ResetBufferInfo(); }
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
diff --git a/src/core/api/multi_radio_api.cpp b/src/core/api/multi_radio_api.cpp
index 10bd547..830456a 100644
--- a/src/core/api/multi_radio_api.cpp
+++ b/src/core/api/multi_radio_api.cpp
@@ -44,8 +44,8 @@
 
 using namespace ot;
 
-otError otMultiRadioGetNeighborInfo(otInstance *              aInstance,
-                                    const otExtAddress *      aExtAddress,
+otError otMultiRadioGetNeighborInfo(otInstance               *aInstance,
+                                    const otExtAddress       *aExtAddress,
                                     otMultiRadioNeighborInfo *aNeighborInfo)
 {
     Error     error = kErrorNone;
diff --git a/src/core/api/nat64_api.cpp b/src/core/api/nat64_api.cpp
new file mode 100644
index 0000000..7405b9d
--- /dev/null
+++ b/src/core/api/nat64_api.cpp
@@ -0,0 +1,176 @@
+/*
+ *  Copyright (c) 2022, 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 the OpenThread APIs for handling IPv4 (NAT64) messages
+ */
+
+#include "openthread-core-config.h"
+
+#include <openthread/border_router.h>
+#include <openthread/ip6.h>
+#include <openthread/nat64.h>
+
+#include "border_router/routing_manager.hpp"
+#include "common/debug.hpp"
+#include "common/instance.hpp"
+#include "net/ip4_types.hpp"
+#include "net/ip6_headers.hpp"
+#include "net/nat64_translator.hpp"
+
+using namespace ot;
+
+// 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
+// separate build flags and they are not depending on each other.
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+otError otNat64SetIp4Cidr(otInstance *aInstance, const otIp4Cidr *aCidr)
+{
+    return AsCoreType(aInstance).Get<Nat64::Translator>().SetIp4Cidr(AsCoreType(aCidr));
+}
+
+otMessage *otIp4NewMessage(otInstance *aInstance, const otMessageSettings *aSettings)
+{
+    return AsCoreType(aInstance).Get<Nat64::Translator>().NewIp4Message(Message::Settings::From(aSettings));
+}
+
+otError otNat64Send(otInstance *aInstance, otMessage *aMessage)
+{
+    return AsCoreType(aInstance).Get<Nat64::Translator>().SendMessage(AsCoreType(aMessage));
+}
+
+void otNat64SetReceiveIp4Callback(otInstance *aInstance, otNat64ReceiveIp4Callback aCallback, void *aContext)
+{
+    AsCoreType(aInstance).Get<Ip6::Ip6>().SetNat64ReceiveIp4DatagramCallback(aCallback, aContext);
+}
+
+void otNat64InitAddressMappingIterator(otInstance *aInstance, otNat64AddressMappingIterator *aIterator)
+{
+    AssertPointerIsNotNull(aIterator);
+
+    AsCoreType(aInstance).Get<Nat64::Translator>().InitAddressMappingIterator(*aIterator);
+}
+
+otError otNat64GetNextAddressMapping(otInstance                    *aInstance,
+                                     otNat64AddressMappingIterator *aIterator,
+                                     otNat64AddressMapping         *aMapping)
+{
+    AssertPointerIsNotNull(aIterator);
+    AssertPointerIsNotNull(aMapping);
+
+    return AsCoreType(aInstance).Get<Nat64::Translator>().GetNextAddressMapping(*aIterator, *aMapping);
+}
+
+void otNat64GetCounters(otInstance *aInstance, otNat64ProtocolCounters *aCounters)
+{
+    AsCoreType(aInstance).Get<Nat64::Translator>().GetCounters(AsCoreType(aCounters));
+}
+
+void otNat64GetErrorCounters(otInstance *aInstance, otNat64ErrorCounters *aCounters)
+{
+    AsCoreType(aInstance).Get<Nat64::Translator>().GetErrorCounters(AsCoreType(aCounters));
+}
+
+otError otNat64GetCidr(otInstance *aInstance, otIp4Cidr *aCidr)
+{
+    return AsCoreType(aInstance).Get<Nat64::Translator>().GetIp4Cidr(AsCoreType(aCidr));
+}
+
+otNat64State otNat64GetTranslatorState(otInstance *aInstance)
+{
+    return MapEnum(AsCoreType(aInstance).Get<Nat64::Translator>().GetState());
+}
+#endif // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+otNat64State otNat64GetPrefixManagerState(otInstance *aInstance)
+{
+    return MapEnum(AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().GetNat64PrefixManagerState());
+}
+#endif
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE || OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+void otNat64SetEnabled(otInstance *aInstance, bool aEnabled)
+{
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+    AsCoreType(aInstance).Get<BorderRouter::RoutingManager>().SetNat64PrefixManagerEnabled(aEnabled);
+#endif
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    AsCoreType(aInstance).Get<Nat64::Translator>().SetEnabled(aEnabled);
+#endif
+}
+#endif // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE || OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+
+bool otIp4IsAddressEqual(const otIp4Address *aFirst, const otIp4Address *aSecond)
+{
+    return AsCoreType(aFirst) == AsCoreType(aSecond);
+}
+
+void otIp4ExtractFromIp6Address(uint8_t aPrefixLength, const otIp6Address *aIp6Address, otIp4Address *aIp4Address)
+{
+    AsCoreType(aIp4Address).ExtractFromIp6Address(aPrefixLength, AsCoreType(aIp6Address));
+}
+
+otError otIp4AddressFromString(const char *aString, otIp4Address *aAddress)
+{
+    AssertPointerIsNotNull(aString);
+    return AsCoreType(aAddress).FromString(aString);
+}
+
+otError otNat64SynthesizeIp6Address(otInstance *aInstance, const otIp4Address *aIp4Address, otIp6Address *aIp6Address)
+{
+    otError                          err = OT_ERROR_NONE;
+    NetworkData::ExternalRouteConfig nat64Prefix;
+
+    VerifyOrExit(AsCoreType(aInstance).Get<NetworkData::Leader>().GetPreferredNat64Prefix(nat64Prefix) == OT_ERROR_NONE,
+                 err = OT_ERROR_INVALID_STATE);
+    AsCoreType(aIp6Address).SynthesizeFromIp4Address(nat64Prefix.GetPrefix(), AsCoreType(aIp4Address));
+
+exit:
+    return err;
+}
+
+void otIp4AddressToString(const otIp4Address *aAddress, char *aBuffer, uint16_t aSize)
+{
+    AssertPointerIsNotNull(aBuffer);
+
+    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);
+
+    AsCoreType(aCidr).ToString(aBuffer, aSize);
+}
diff --git a/src/core/api/netdata_api.cpp b/src/core/api/netdata_api.cpp
index c28b987..28e06c7 100644
--- a/src/core/api/netdata_api.cpp
+++ b/src/core/api/netdata_api.cpp
@@ -42,22 +42,35 @@
 
 otError otNetDataGet(otInstance *aInstance, bool aStable, uint8_t *aData, uint8_t *aDataLength)
 {
+    AssertPointerIsNotNull(aData);
+    AssertPointerIsNotNull(aDataLength);
+
     return AsCoreType(aInstance).Get<NetworkData::Leader>().CopyNetworkData(
         aStable ? NetworkData::kStableSubset : NetworkData::kFullSet, aData, *aDataLength);
 }
 
-otError otNetDataGetNextOnMeshPrefix(otInstance *           aInstance,
-                                     otNetworkDataIterator *aIterator,
-                                     otBorderRouterConfig * aConfig)
+uint8_t otNetDataGetLength(otInstance *aInstance)
 {
-    Error error = kErrorNone;
+    return AsCoreType(aInstance).Get<NetworkData::Leader>().GetLength();
+}
 
-    VerifyOrExit(aIterator && aConfig, error = kErrorInvalidArgs);
+uint8_t otNetDataGetMaxLength(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<NetworkData::Leader>().GetMaxLength();
+}
 
-    error = AsCoreType(aInstance).Get<NetworkData::Leader>().GetNextOnMeshPrefix(*aIterator, AsCoreType(aConfig));
+void otNetDataResetMaxLength(otInstance *aInstance)
+{
+    AsCoreType(aInstance).Get<NetworkData::Leader>().ResetMaxLength();
+}
 
-exit:
-    return error;
+otError otNetDataGetNextOnMeshPrefix(otInstance            *aInstance,
+                                     otNetworkDataIterator *aIterator,
+                                     otBorderRouterConfig  *aConfig)
+{
+    AssertPointerIsNotNull(aIterator);
+
+    return AsCoreType(aInstance).Get<NetworkData::Leader>().GetNextOnMeshPrefix(*aIterator, AsCoreType(aConfig));
 }
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
@@ -69,26 +82,26 @@
 
 otError otNetDataGetNextRoute(otInstance *aInstance, otNetworkDataIterator *aIterator, otExternalRouteConfig *aConfig)
 {
-    Error error = kErrorNone;
+    AssertPointerIsNotNull(aIterator);
 
-    VerifyOrExit(aIterator && aConfig, error = kErrorInvalidArgs);
-
-    error = AsCoreType(aInstance).Get<NetworkData::Leader>().GetNextExternalRoute(*aIterator, AsCoreType(aConfig));
-
-exit:
-    return error;
+    return AsCoreType(aInstance).Get<NetworkData::Leader>().GetNextExternalRoute(*aIterator, AsCoreType(aConfig));
 }
 
 otError otNetDataGetNextService(otInstance *aInstance, otNetworkDataIterator *aIterator, otServiceConfig *aConfig)
 {
-    Error error = kErrorNone;
+    AssertPointerIsNotNull(aIterator);
 
-    VerifyOrExit(aIterator && aConfig, error = kErrorInvalidArgs);
+    return AsCoreType(aInstance).Get<NetworkData::Leader>().GetNextService(*aIterator, AsCoreType(aConfig));
+}
 
-    error = AsCoreType(aInstance).Get<NetworkData::Leader>().GetNextService(*aIterator, AsCoreType(aConfig));
+otError otNetDataGetNextLowpanContextInfo(otInstance            *aInstance,
+                                          otNetworkDataIterator *aIterator,
+                                          otLowpanContextInfo   *aContextInfo)
+{
+    AssertPointerIsNotNull(aIterator);
 
-exit:
-    return error;
+    return AsCoreType(aInstance).Get<NetworkData::Leader>().GetNextLowpanContextInfo(*aIterator,
+                                                                                     AsCoreType(aContextInfo));
 }
 
 uint8_t otNetDataGetVersion(otInstance *aInstance)
diff --git a/src/core/api/netdata_publisher_api.cpp b/src/core/api/netdata_publisher_api.cpp
index 559734b..9101b0a 100644
--- a/src/core/api/netdata_publisher_api.cpp
+++ b/src/core/api/netdata_publisher_api.cpp
@@ -64,9 +64,9 @@
     return AsCoreType(aInstance).Get<NetworkData::Publisher>().IsDnsSrpServiceAdded();
 }
 
-void otNetDataSetDnsSrpServicePublisherCallback(otInstance *                            aInstance,
+void otNetDataSetDnsSrpServicePublisherCallback(otInstance                             *aInstance,
                                                 otNetDataDnsSrpServicePublisherCallback aCallback,
-                                                void *                                  aContext)
+                                                void                                   *aContext)
 {
     AsCoreType(aInstance).Get<NetworkData::Publisher>().SetDnsSrpServiceCallback(aCallback, aContext);
 }
@@ -82,12 +82,22 @@
 
 otError otNetDataPublishOnMeshPrefix(otInstance *aInstance, const otBorderRouterConfig *aConfig)
 {
-    return AsCoreType(aInstance).Get<NetworkData::Publisher>().PublishOnMeshPrefix(AsCoreType(aConfig));
+    return AsCoreType(aInstance).Get<NetworkData::Publisher>().PublishOnMeshPrefix(AsCoreType(aConfig),
+                                                                                   NetworkData::Publisher::kFromUser);
 }
 
 otError otNetDataPublishExternalRoute(otInstance *aInstance, const otExternalRouteConfig *aConfig)
 {
-    return AsCoreType(aInstance).Get<NetworkData::Publisher>().PublishExternalRoute(AsCoreType(aConfig));
+    return AsCoreType(aInstance).Get<NetworkData::Publisher>().PublishExternalRoute(AsCoreType(aConfig),
+                                                                                    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)
@@ -95,9 +105,9 @@
     return AsCoreType(aInstance).Get<NetworkData::Publisher>().IsPrefixAdded(AsCoreType(aPrefix));
 }
 
-void otNetDataSetPrefixPublisherCallback(otInstance *                     aInstance,
+void otNetDataSetPrefixPublisherCallback(otInstance                      *aInstance,
                                          otNetDataPrefixPublisherCallback aCallback,
-                                         void *                           aContext)
+                                         void                            *aContext)
 {
     return AsCoreType(aInstance).Get<NetworkData::Publisher>().SetPrefixCallback(aCallback, aContext);
 }
diff --git a/src/core/api/netdiag_api.cpp b/src/core/api/netdiag_api.cpp
index ff09c8e..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,31 +40,68 @@
 
 using namespace ot;
 
-otError otThreadGetNextDiagnosticTlv(const otMessage *      aMessage,
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+
+otError otThreadGetNextDiagnosticTlv(const otMessage       *aMessage,
                                      otNetworkDiagIterator *aIterator,
-                                     otNetworkDiagTlv *     aNetworkDiagTlv)
+                                     otNetworkDiagTlv      *aNetworkDiagTlv)
 {
-    return NetworkDiagnostic::NetworkDiagnostic::GetNextDiagTlv(AsCoapMessage(aMessage), *aIterator, *aNetworkDiagTlv);
+    AssertPointerIsNotNull(aIterator);
+    AssertPointerIsNotNull(aNetworkDiagTlv);
+
+    return NetworkDiagnostic::Client::GetNextDiagTlv(AsCoapMessage(aMessage), *aIterator, *aNetworkDiagTlv);
 }
 
-otError otThreadSendDiagnosticGet(otInstance *                   aInstance,
-                                  const otIp6Address *           aDestination,
+otError otThreadSendDiagnosticGet(otInstance                    *aInstance,
+                                  const otIp6Address            *aDestination,
                                   const uint8_t                  aTlvTypes[],
                                   uint8_t                        aCount,
                                   otReceiveDiagnosticGetCallback aCallback,
-                                  void *                         aCallbackContext)
+                                  void                          *aCallbackContext)
 {
-    return AsCoreType(aInstance).Get<NetworkDiagnostic::NetworkDiagnostic>().SendDiagnosticGet(
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Client>().SendDiagnosticGet(
         AsCoreType(aDestination), aTlvTypes, aCount, aCallback, aCallbackContext);
 }
 
-otError otThreadSendDiagnosticReset(otInstance *        aInstance,
+otError otThreadSendDiagnosticReset(otInstance         *aInstance,
                                     const otIp6Address *aDestination,
                                     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/network_time_api.cpp b/src/core/api/network_time_api.cpp
index 128aff4..903ed86 100644
--- a/src/core/api/network_time_api.cpp
+++ b/src/core/api/network_time_api.cpp
@@ -44,7 +44,9 @@
 
 otNetworkTimeStatus otNetworkTimeGet(otInstance *aInstance, uint64_t *aNetworkTime)
 {
-    return AsCoreType(aInstance).Get<TimeSync>().GetTime(*aNetworkTime);
+    AssertPointerIsNotNull(aNetworkTime);
+
+    return MapEnum(AsCoreType(aInstance).Get<TimeSync>().GetTime(*aNetworkTime));
 }
 
 otError otNetworkTimeSetSyncPeriod(otInstance *aInstance, uint16_t aTimeSyncPeriod)
diff --git a/src/core/api/ping_sender_api.cpp b/src/core/api/ping_sender_api.cpp
index 9172ed4..3319306 100644
--- a/src/core/api/ping_sender_api.cpp
+++ b/src/core/api/ping_sender_api.cpp
@@ -47,9 +47,6 @@
     return AsCoreType(aInstance).Get<Utils::PingSender>().Ping(AsCoreType(aConfig));
 }
 
-void otPingSenderStop(otInstance *aInstance)
-{
-    AsCoreType(aInstance).Get<Utils::PingSender>().Stop();
-}
+void otPingSenderStop(otInstance *aInstance) { AsCoreType(aInstance).Get<Utils::PingSender>().Stop(); }
 
 #endif // OPENTHREAD_CONFIG_PING_SENDER_ENABLE
diff --git a/src/core/api/random_noncrypto_api.cpp b/src/core/api/random_noncrypto_api.cpp
index 51fb141..6a2d18b 100644
--- a/src/core/api/random_noncrypto_api.cpp
+++ b/src/core/api/random_noncrypto_api.cpp
@@ -37,20 +37,11 @@
 
 using namespace ot;
 
-uint32_t otRandomNonCryptoGetUint32(void)
-{
-    return Random::NonCrypto::GetUint32();
-}
+uint32_t otRandomNonCryptoGetUint32(void) { return Random::NonCrypto::GetUint32(); }
 
-uint8_t otRandomNonCryptoGetUint8(void)
-{
-    return Random::NonCrypto::GetUint8();
-}
+uint8_t otRandomNonCryptoGetUint8(void) { return Random::NonCrypto::GetUint8(); }
 
-uint16_t otRandomNonCryptoGetUint16(void)
-{
-    return Random::NonCrypto::GetUint16();
-}
+uint16_t otRandomNonCryptoGetUint16(void) { return Random::NonCrypto::GetUint16(); }
 
 uint8_t otRandomNonCryptoGetUint8InRange(uint8_t aMin, uint8_t aMax)
 {
@@ -67,10 +58,7 @@
     return Random::NonCrypto::GetUint32InRange(aMin, aMax);
 }
 
-void otRandomNonCryptoFillBuffer(uint8_t *aBuffer, uint16_t aSize)
-{
-    Random::NonCrypto::FillBuffer(aBuffer, aSize);
-}
+void otRandomNonCryptoFillBuffer(uint8_t *aBuffer, uint16_t aSize) { Random::NonCrypto::FillBuffer(aBuffer, aSize); }
 
 uint32_t otRandomNonCryptoAddJitter(uint32_t aValue, uint16_t aJitter)
 {
diff --git a/src/core/api/server_api.cpp b/src/core/api/server_api.cpp
index b1712f1..9c44ab8 100644
--- a/src/core/api/server_api.cpp
+++ b/src/core/api/server_api.cpp
@@ -60,7 +60,7 @@
                                                                       aConfig->mServerConfig.mStable, serverData);
 }
 
-otError otServerRemoveService(otInstance *   aInstance,
+otError otServerRemoveService(otInstance    *aInstance,
                               uint32_t       aEnterpriseNumber,
                               const uint8_t *aServiceData,
                               uint8_t        aServiceDataLength)
diff --git a/src/core/api/sntp_api.cpp b/src/core/api/sntp_api.cpp
index 007fc92..1651c3b 100644
--- a/src/core/api/sntp_api.cpp
+++ b/src/core/api/sntp_api.cpp
@@ -42,10 +42,10 @@
 
 using namespace ot;
 
-otError otSntpClientQuery(otInstance *          aInstance,
-                          const otSntpQuery *   aQuery,
+otError otSntpClientQuery(otInstance           *aInstance,
+                          const otSntpQuery    *aQuery,
                           otSntpResponseHandler aHandler,
-                          void *                aContext)
+                          void                 *aContext)
 {
     return AsCoreType(aInstance).Get<Sntp::Client>().Query(aQuery, aHandler, aContext);
 }
diff --git a/src/core/api/srp_client_api.cpp b/src/core/api/srp_client_api.cpp
index 3ec356f..7078c20 100644
--- a/src/core/api/srp_client_api.cpp
+++ b/src/core/api/srp_client_api.cpp
@@ -47,15 +47,9 @@
     return AsCoreType(aInstance).Get<Srp::Client>().Start(AsCoreType(aServerSockAddr));
 }
 
-void otSrpClientStop(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Srp::Client>().Stop();
-}
+void otSrpClientStop(otInstance *aInstance) { return AsCoreType(aInstance).Get<Srp::Client>().Stop(); }
 
-bool otSrpClientIsRunning(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Srp::Client>().IsRunning();
-}
+bool otSrpClientIsRunning(otInstance *aInstance) { return AsCoreType(aInstance).Get<Srp::Client>().IsRunning(); }
 
 const otSockAddr *otSrpClientGetServerAddress(otInstance *aInstance)
 {
@@ -84,10 +78,7 @@
 }
 #endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
 
-uint32_t otSrpClientGetTtl(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Srp::Client>().GetTtl();
-}
+uint32_t otSrpClientGetTtl(otInstance *aInstance) { return AsCoreType(aInstance).Get<Srp::Client>().GetTtl(); }
 
 void otSrpClientSetTtl(otInstance *aInstance, uint32_t aTtl)
 {
diff --git a/src/core/api/srp_client_buffers_api.cpp b/src/core/api/srp_client_buffers_api.cpp
index 51c91a8..35b2605 100644
--- a/src/core/api/srp_client_buffers_api.cpp
+++ b/src/core/api/srp_client_buffers_api.cpp
@@ -44,11 +44,15 @@
 
 char *otSrpClientBuffersGetHostNameString(otInstance *aInstance, uint16_t *aSize)
 {
+    AssertPointerIsNotNull(aSize);
+
     return AsCoreType(aInstance).Get<Utils::SrpClientBuffers>().GetHostNameString(*aSize);
 }
 
 otIp6Address *otSrpClientBuffersGetHostAddressesArray(otInstance *aInstance, uint8_t *aArrayLength)
 {
+    AssertPointerIsNotNull(aArrayLength);
+
     return AsCoreType(aInstance).Get<Utils::SrpClientBuffers>().GetHostAddressesArray(*aArrayLength);
 }
 
@@ -69,21 +73,29 @@
 
 char *otSrpClientBuffersGetServiceEntryServiceNameString(otSrpClientBuffersServiceEntry *aEntry, uint16_t *aSize)
 {
+    AssertPointerIsNotNull(aSize);
+
     return AsCoreType(aEntry).GetServiceNameString(*aSize);
 }
 
 char *otSrpClientBuffersGetServiceEntryInstanceNameString(otSrpClientBuffersServiceEntry *aEntry, uint16_t *aSize)
 {
+    AssertPointerIsNotNull(aSize);
+
     return AsCoreType(aEntry).GetInstanceNameString(*aSize);
 }
 
 uint8_t *otSrpClientBuffersGetServiceEntryTxtBuffer(otSrpClientBuffersServiceEntry *aEntry, uint16_t *aSize)
 {
+    AssertPointerIsNotNull(aSize);
+
     return AsCoreType(aEntry).GetTxtBuffer(*aSize);
 }
 
 const char **otSrpClientBuffersGetSubTypeLabelsArray(otSrpClientBuffersServiceEntry *aEntry, uint16_t *aArrayLength)
 {
+    AssertPointerIsNotNull(aArrayLength);
+
     return AsCoreType(aEntry).GetSubTypeLabelsArray(*aArrayLength);
 }
 
diff --git a/src/core/api/srp_server_api.cpp b/src/core/api/srp_server_api.cpp
index f9d4367..a6a3ca7 100644
--- a/src/core/api/srp_server_api.cpp
+++ b/src/core/api/srp_server_api.cpp
@@ -42,10 +42,7 @@
 
 using namespace ot;
 
-const char *otSrpServerGetDomain(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Srp::Server>().GetDomain();
-}
+const char *otSrpServerGetDomain(otInstance *aInstance) { return AsCoreType(aInstance).Get<Srp::Server>().GetDomain(); }
 
 otError otSrpServerSetDomain(otInstance *aInstance, const char *aDomain)
 {
@@ -57,10 +54,7 @@
     return MapEnum(AsCoreType(aInstance).Get<Srp::Server>().GetState());
 }
 
-uint16_t otSrpServerGetPort(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Srp::Server>().GetPort();
-}
+uint16_t otSrpServerGetPort(otInstance *aInstance) { return AsCoreType(aInstance).Get<Srp::Server>().GetPort(); }
 
 otSrpServerAddressMode otSrpServerGetAddressMode(otInstance *aInstance)
 {
@@ -87,6 +81,18 @@
     AsCoreType(aInstance).Get<Srp::Server>().SetEnabled(aEnabled);
 }
 
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+void otSrpServerSetAutoEnableMode(otInstance *aInstance, bool aEnabled)
+{
+    AsCoreType(aInstance).Get<Srp::Server>().SetAutoEnableMode(aEnabled);
+}
+
+bool otSrpServerIsAutoEnableMode(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<Srp::Server>().IsAutoEnableMode();
+}
+#endif
+
 void otSrpServerGetTtlConfig(otInstance *aInstance, otSrpServerTtlConfig *aTtlConfig)
 {
     AsCoreType(aInstance).Get<Srp::Server>().GetTtlConfig(AsCoreType(aTtlConfig));
@@ -107,9 +113,9 @@
     return AsCoreType(aInstance).Get<Srp::Server>().SetLeaseConfig(AsCoreType(aLeaseConfig));
 }
 
-void otSrpServerSetServiceUpdateHandler(otInstance *                    aInstance,
+void otSrpServerSetServiceUpdateHandler(otInstance                     *aInstance,
                                         otSrpServerServiceUpdateHandler aServiceHandler,
-                                        void *                          aContext)
+                                        void                           *aContext)
 {
     AsCoreType(aInstance).Get<Srp::Server>().SetServiceHandler(aServiceHandler, aContext);
 }
@@ -129,15 +135,9 @@
     return AsCoreType(aInstance).Get<Srp::Server>().GetResponseCounters();
 }
 
-bool otSrpServerHostIsDeleted(const otSrpServerHost *aHost)
-{
-    return AsCoreType(aHost).IsDeleted();
-}
+bool otSrpServerHostIsDeleted(const otSrpServerHost *aHost) { return AsCoreType(aHost).IsDeleted(); }
 
-const char *otSrpServerHostGetFullName(const otSrpServerHost *aHost)
-{
-    return AsCoreType(aHost).GetFullName();
-}
+const char *otSrpServerHostGetFullName(const otSrpServerHost *aHost) { return AsCoreType(aHost).GetFullName(); }
 
 const otIp6Address *otSrpServerHostGetAddresses(const otSrpServerHost *aHost, uint8_t *aAddressesNum)
 {
@@ -149,35 +149,26 @@
     AsCoreType(aHost).GetLeaseInfo(*aLeaseInfo);
 }
 
-uint32_t otSrpServerHostGetKeyLease(const otSrpServerHost *aHost)
-{
-    return AsCoreType(aHost).GetKeyLease();
-}
+uint32_t otSrpServerHostGetKeyLease(const otSrpServerHost *aHost) { return AsCoreType(aHost).GetKeyLease(); }
 
-const otSrpServerService *otSrpServerHostGetNextService(const otSrpServerHost *   aHost,
+const otSrpServerService *otSrpServerHostGetNextService(const otSrpServerHost    *aHost,
                                                         const otSrpServerService *aService)
 {
     return AsCoreType(aHost).FindNextService(AsCoreTypePtr(aService), Srp::Server::kFlagsBaseTypeServiceOnly);
 }
 
-const otSrpServerService *otSrpServerHostFindNextService(const otSrpServerHost *   aHost,
+const otSrpServerService *otSrpServerHostFindNextService(const otSrpServerHost    *aHost,
                                                          const otSrpServerService *aPrevService,
                                                          otSrpServerServiceFlags   aFlags,
-                                                         const char *              aServiceName,
-                                                         const char *              aInstanceName)
+                                                         const char               *aServiceName,
+                                                         const char               *aInstanceName)
 {
     return AsCoreType(aHost).FindNextService(AsCoreTypePtr(aPrevService), aFlags, aServiceName, aInstanceName);
 }
 
-bool otSrpServerServiceIsDeleted(const otSrpServerService *aService)
-{
-    return AsCoreType(aService).IsDeleted();
-}
+bool otSrpServerServiceIsDeleted(const otSrpServerService *aService) { return AsCoreType(aService).IsDeleted(); }
 
-bool otSrpServerServiceIsSubType(const otSrpServerService *aService)
-{
-    return AsCoreType(aService).IsSubType();
-}
+bool otSrpServerServiceIsSubType(const otSrpServerService *aService) { return AsCoreType(aService).IsSubType(); }
 
 const char *otSrpServerServiceGetFullName(const otSrpServerService *aService)
 {
@@ -199,25 +190,16 @@
     return AsCoreType(aService).GetServiceSubTypeLabel(aLabel, aMaxSize);
 }
 
-uint16_t otSrpServerServiceGetPort(const otSrpServerService *aService)
-{
-    return AsCoreType(aService).GetPort();
-}
+uint16_t otSrpServerServiceGetPort(const otSrpServerService *aService) { return AsCoreType(aService).GetPort(); }
 
-uint16_t otSrpServerServiceGetWeight(const otSrpServerService *aService)
-{
-    return AsCoreType(aService).GetWeight();
-}
+uint16_t otSrpServerServiceGetWeight(const otSrpServerService *aService) { return AsCoreType(aService).GetWeight(); }
 
 uint16_t otSrpServerServiceGetPriority(const otSrpServerService *aService)
 {
     return AsCoreType(aService).GetPriority();
 }
 
-uint32_t otSrpServerServiceGetTtl(const otSrpServerService *aService)
-{
-    return AsCoreType(aService).GetTtl();
-}
+uint32_t otSrpServerServiceGetTtl(const otSrpServerService *aService) { return AsCoreType(aService).GetTtl(); }
 
 const uint8_t *otSrpServerServiceGetTxtData(const otSrpServerService *aService, uint16_t *aDataLength)
 {
diff --git a/src/core/api/tasklet_api.cpp b/src/core/api/tasklet_api.cpp
index e5213c8..bcda24a 100644
--- a/src/core/api/tasklet_api.cpp
+++ b/src/core/api/tasklet_api.cpp
@@ -60,6 +60,4 @@
     return retval;
 }
 
-OT_TOOL_WEAK void otTaskletsSignalPending(otInstance *)
-{
-}
+OT_TOOL_WEAK void otTaskletsSignalPending(otInstance *) {}
diff --git a/src/core/api/tcp_api.cpp b/src/core/api/tcp_api.cpp
index 896aac6..b779d48 100644
--- a/src/core/api/tcp_api.cpp
+++ b/src/core/api/tcp_api.cpp
@@ -42,22 +42,16 @@
 
 using namespace ot;
 
-otError otTcpEndpointInitialize(otInstance *                       aInstance,
-                                otTcpEndpoint *                    aEndpoint,
+otError otTcpEndpointInitialize(otInstance                        *aInstance,
+                                otTcpEndpoint                     *aEndpoint,
                                 const otTcpEndpointInitializeArgs *aArgs)
 {
     return AsCoreType(aEndpoint).Initialize(AsCoreType(aInstance), *aArgs);
 }
 
-otInstance *otTcpEndpointGetInstance(otTcpEndpoint *aEndpoint)
-{
-    return &AsCoreType(aEndpoint).GetInstance();
-}
+otInstance *otTcpEndpointGetInstance(otTcpEndpoint *aEndpoint) { return &AsCoreType(aEndpoint).GetInstance(); }
 
-void *otTcpEndpointGetContext(otTcpEndpoint *aEndpoint)
-{
-    return AsCoreType(aEndpoint).GetContext();
-}
+void *otTcpEndpointGetContext(otTcpEndpoint *aEndpoint) { return AsCoreType(aEndpoint).GetContext(); }
 
 const otSockAddr *otTcpGetLocalAddress(const otTcpEndpoint *aEndpoint)
 {
@@ -94,61 +88,37 @@
     return AsCoreType(aEndpoint).ReceiveByReference(*aBuffer);
 }
 
-otError otTcpReceiveContiguify(otTcpEndpoint *aEndpoint)
-{
-    return AsCoreType(aEndpoint).ReceiveContiguify();
-}
+otError otTcpReceiveContiguify(otTcpEndpoint *aEndpoint) { return AsCoreType(aEndpoint).ReceiveContiguify(); }
 
 otError otTcpCommitReceive(otTcpEndpoint *aEndpoint, size_t aNumBytes, uint32_t aFlags)
 {
     return AsCoreType(aEndpoint).CommitReceive(aNumBytes, aFlags);
 }
 
-otError otTcpSendEndOfStream(otTcpEndpoint *aEndpoint)
-{
-    return AsCoreType(aEndpoint).SendEndOfStream();
-}
+otError otTcpSendEndOfStream(otTcpEndpoint *aEndpoint) { return AsCoreType(aEndpoint).SendEndOfStream(); }
 
-otError otTcpAbort(otTcpEndpoint *aEndpoint)
-{
-    return AsCoreType(aEndpoint).Abort();
-}
+otError otTcpAbort(otTcpEndpoint *aEndpoint) { return AsCoreType(aEndpoint).Abort(); }
 
-otError otTcpEndpointDeinitialize(otTcpEndpoint *aEndpoint)
-{
-    return AsCoreType(aEndpoint).Deinitialize();
-}
+otError otTcpEndpointDeinitialize(otTcpEndpoint *aEndpoint) { return AsCoreType(aEndpoint).Deinitialize(); }
 
-otError otTcpListenerInitialize(otInstance *                       aInstance,
-                                otTcpListener *                    aListener,
+otError otTcpListenerInitialize(otInstance                        *aInstance,
+                                otTcpListener                     *aListener,
                                 const otTcpListenerInitializeArgs *aArgs)
 {
     return AsCoreType(aListener).Initialize(AsCoreType(aInstance), *aArgs);
 }
 
-otInstance *otTcpListenerGetInstance(otTcpListener *aListener)
-{
-    return &AsCoreType(aListener).GetInstance();
-}
+otInstance *otTcpListenerGetInstance(otTcpListener *aListener) { return &AsCoreType(aListener).GetInstance(); }
 
-void *otTcpListenerGetContext(otTcpListener *aListener)
-{
-    return AsCoreType(aListener).GetContext();
-}
+void *otTcpListenerGetContext(otTcpListener *aListener) { return AsCoreType(aListener).GetContext(); }
 
 otError otTcpListen(otTcpListener *aListener, const otSockAddr *aSockName)
 {
     return AsCoreType(aListener).Listen(AsCoreType(aSockName));
 }
 
-otError otTcpStopListening(otTcpListener *aListener)
-{
-    return AsCoreType(aListener).StopListening();
-}
+otError otTcpStopListening(otTcpListener *aListener) { return AsCoreType(aListener).StopListening(); }
 
-otError otTcpListenerDeinitialize(otTcpListener *aListener)
-{
-    return AsCoreType(aListener).Deinitialize();
-}
+otError otTcpListenerDeinitialize(otTcpListener *aListener) { return AsCoreType(aListener).Deinitialize(); }
 
 #endif // OPENTHREAD_CONFIG_TCP_ENABLE
diff --git a/src/core/api/tcp_ext_api.cpp b/src/core/api/tcp_ext_api.cpp
new file mode 100644
index 0000000..c654066
--- /dev/null
+++ b/src/core/api/tcp_ext_api.cpp
@@ -0,0 +1,136 @@
+/*
+ *  Copyright (c) 2022, 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 extensions to the OpenThread TCP API.
+ */
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_TCP_ENABLE
+
+#include <openthread/tcp_ext.h>
+
+#include "common/as_core_type.hpp"
+#include "common/locator_getters.hpp"
+#include "net/tcp6_ext.hpp"
+
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+
+#include <mbedtls/ssl.h>
+
+#endif
+
+using namespace ot;
+
+void otTcpCircularSendBufferInitialize(otTcpCircularSendBuffer *aSendBuffer, void *aDataBuffer, size_t aCapacity)
+{
+    AsCoreType(aSendBuffer).Initialize(aDataBuffer, aCapacity);
+}
+
+otError otTcpCircularSendBufferWrite(otTcpEndpoint           *aEndpoint,
+                                     otTcpCircularSendBuffer *aSendBuffer,
+                                     const void              *aData,
+                                     size_t                   aLength,
+                                     size_t                  *aWritten,
+                                     uint32_t                 aFlags)
+{
+    AssertPointerIsNotNull(aWritten);
+    return AsCoreType(aSendBuffer).Write(AsCoreType(aEndpoint), aData, aLength, *aWritten, aFlags);
+}
+
+void otTcpCircularSendBufferHandleForwardProgress(otTcpCircularSendBuffer *aSendBuffer, size_t aInSendBuffer)
+{
+    AsCoreType(aSendBuffer).HandleForwardProgress(aInSendBuffer);
+}
+
+size_t otTcpCircularSendBufferGetFreeSpace(const otTcpCircularSendBuffer *aSendBuffer)
+{
+    return AsCoreType(aSendBuffer).GetFreeSpace();
+}
+
+void otTcpCircularSendBufferForceDiscardAll(otTcpCircularSendBuffer *aSendBuffer)
+{
+    AsCoreType(aSendBuffer).ForceDiscardAll();
+}
+
+otError otTcpCircularSendBufferDeinitialize(otTcpCircularSendBuffer *aSendBuffer)
+{
+    return AsCoreType(aSendBuffer).Deinitialize();
+}
+
+#if OPENTHREAD_CONFIG_TLS_ENABLE
+
+int otTcpMbedTlsSslSendCallback(void *aCtx, const unsigned char *aBuf, size_t aLen)
+{
+    otTcpEndpointAndCircularSendBuffer *pair       = static_cast<otTcpEndpointAndCircularSendBuffer *>(aCtx);
+    otTcpEndpoint                      *endpoint   = pair->mEndpoint;
+    otTcpCircularSendBuffer            *sendBuffer = pair->mSendBuffer;
+    size_t                              bytes_written;
+    int                                 result;
+    otError                             error;
+
+    error = otTcpCircularSendBufferWrite(endpoint, sendBuffer, aBuf, aLen, &bytes_written, 0);
+    VerifyOrExit(error == OT_ERROR_NONE, result = MBEDTLS_ERR_SSL_INTERNAL_ERROR);
+    VerifyOrExit(aLen == 0 || bytes_written != 0, result = MBEDTLS_ERR_SSL_WANT_WRITE);
+    result = static_cast<int>(bytes_written);
+
+exit:
+    return result;
+}
+
+int otTcpMbedTlsSslRecvCallback(void *aCtx, unsigned char *aBuf, size_t aLen)
+{
+    otTcpEndpointAndCircularSendBuffer *pair       = static_cast<otTcpEndpointAndCircularSendBuffer *>(aCtx);
+    otTcpEndpoint                      *endpoint   = pair->mEndpoint;
+    size_t                              bytes_read = 0;
+    const otLinkedBuffer               *buffer;
+    int                                 result;
+    otError                             error;
+
+    error = otTcpReceiveByReference(endpoint, &buffer);
+    VerifyOrExit(error == OT_ERROR_NONE, result = MBEDTLS_ERR_SSL_INTERNAL_ERROR);
+    while (bytes_read != aLen && buffer != nullptr)
+    {
+        size_t to_copy = OT_MIN(aLen - bytes_read, buffer->mLength);
+        memcpy(&aBuf[bytes_read], buffer->mData, to_copy);
+        bytes_read += to_copy;
+        buffer = buffer->mNext;
+    }
+    VerifyOrExit(aLen == 0 || bytes_read != 0, result = MBEDTLS_ERR_SSL_WANT_READ);
+    IgnoreReturnValue(otTcpCommitReceive(endpoint, bytes_read, 0));
+    result = static_cast<int>(bytes_read);
+
+exit:
+    return result;
+}
+
+#endif // OPENTHREAD_CONFIG_TLS_ENABLE
+
+#endif // OPENTHREAD_CONFIG_TCP_ENABLE
diff --git a/src/core/api/thread_api.cpp b/src/core/api/thread_api.cpp
index bec8c06..778a663 100644
--- a/src/core/api/thread_api.cpp
+++ b/src/core/api/thread_api.cpp
@@ -40,6 +40,8 @@
 #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;
 
@@ -61,7 +63,7 @@
 otError otThreadSetExtendedPanId(otInstance *aInstance, const otExtendedPanId *aExtendedPanId)
 {
     Error                         error    = kErrorNone;
-    Instance &                    instance = AsCoreType(aInstance);
+    Instance                     &instance = AsCoreType(aInstance);
     const MeshCoP::ExtendedPanId &extPanId = AsCoreType(aExtendedPanId);
 
     VerifyOrExit(instance.Get<Mle::MleRouter>().IsDisabled(), error = kErrorInvalidState);
@@ -77,8 +79,6 @@
 
 otError otThreadGetLeaderRloc(otInstance *aInstance, otIp6Address *aLeaderRloc)
 {
-    OT_ASSERT(aLeaderRloc != nullptr);
-
     return AsCoreType(aInstance).Get<Mle::MleRouter>().GetLeaderAddress(AsCoreType(aLeaderRloc));
 }
 
@@ -113,8 +113,6 @@
     Error     error    = kErrorNone;
     Instance &instance = AsCoreType(aInstance);
 
-    OT_ASSERT(aKey != nullptr);
-
     VerifyOrExit(instance.Get<Mle::MleRouter>().IsDisabled(), error = kErrorInvalidState);
 
     instance.Get<KeyManager>().SetNetworkKey(AsCoreType(aKey));
@@ -205,6 +203,12 @@
 
     VerifyOrExit(AsCoreType(aInstance).Get<Mle::MleRouter>().IsDisabled(), error = kErrorInvalidState);
 
+#if !OPENTHREAD_CONFIG_ALLOW_EMPTY_NETWORK_NAME
+    // Thread interfaces support a zero length name internally for backwards compatibility, but new names
+    // must be at least one valid character long.
+    VerifyOrExit(nullptr != aNetworkName && aNetworkName[0] != '\0', error = kErrorInvalidArgs);
+#endif
+
     error = AsCoreType(aInstance).Get<MeshCoP::NetworkNameManager>().SetNetworkName(aNetworkName);
     AsCoreType(aInstance).Get<MeshCoP::ActiveDatasetManager>().Clear();
     AsCoreType(aInstance).Get<MeshCoP::PendingDatasetManager>().Clear();
@@ -250,7 +254,7 @@
 
 const otIp6InterfaceIdentifier *otThreadGetFixedDuaInterfaceIdentifier(otInstance *aInstance)
 {
-    Instance &                      instance = AsCoreType(aInstance);
+    Instance                       &instance = AsCoreType(aInstance);
     const otIp6InterfaceIdentifier *iid      = nullptr;
 
     if (instance.Get<DuaManager>().IsFixedDuaInterfaceIdentifierSet())
@@ -289,14 +293,11 @@
     return AsCoreType(aInstance).Get<Mle::MleRouter>().BecomeDetached();
 }
 
-otError otThreadBecomeChild(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mle::MleRouter>().BecomeChild();
-}
+otError otThreadBecomeChild(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mle::MleRouter>().BecomeChild(); }
 
 otError otThreadGetNextNeighborInfo(otInstance *aInstance, otNeighborInfoIterator *aIterator, otNeighborInfo *aInfo)
 {
-    OT_ASSERT((aInfo != nullptr) && (aIterator != nullptr));
+    AssertPointerIsNotNull(aIterator);
 
     return AsCoreType(aInstance).Get<NeighborTable>().GetNextNeighborInfo(*aIterator, AsCoreType(aInfo));
 }
@@ -306,16 +307,13 @@
     return MapEnum(AsCoreType(aInstance).Get<Mle::MleRouter>().GetRole());
 }
 
-const char *otThreadDeviceRoleToString(otDeviceRole aRole)
-{
-    return Mle::Mle::RoleToString(MapEnum(aRole));
-}
+const char *otThreadDeviceRoleToString(otDeviceRole aRole) { return Mle::RoleToString(MapEnum(aRole)); }
 
 otError otThreadGetLeaderData(otInstance *aInstance, otLeaderData *aLeaderData)
 {
     Error error = kErrorNone;
 
-    OT_ASSERT(aLeaderData != nullptr);
+    AssertPointerIsNotNull(aLeaderData);
 
     VerifyOrExit(AsCoreType(aInstance).Get<Mle::MleRouter>().IsAttached(), error = kErrorDetached);
     *aLeaderData = AsCoreType(aInstance).Get<Mle::MleRouter>().GetLeaderData();
@@ -339,51 +337,22 @@
     return AsCoreType(aInstance).Get<Mle::MleRouter>().GetLeaderData().GetPartitionId();
 }
 
-uint16_t otThreadGetRloc16(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mle::MleRouter>().GetRloc16();
-}
+uint16_t otThreadGetRloc16(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mle::MleRouter>().GetRloc16(); }
 
 otError otThreadGetParentInfo(otInstance *aInstance, otRouterInfo *aParentInfo)
 {
-    Error   error = kErrorNone;
-    Router *parent;
-
-    OT_ASSERT(aParentInfo != nullptr);
-
-    // Reference device needs get the original parent's info even after the node state changed.
-#if !OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-    VerifyOrExit(AsCoreType(aInstance).Get<Mle::MleRouter>().IsChild(), error = kErrorInvalidState);
-#endif
-
-    parent = &AsCoreType(aInstance).Get<Mle::MleRouter>().GetParent();
-
-    aParentInfo->mExtAddress     = parent->GetExtAddress();
-    aParentInfo->mRloc16         = parent->GetRloc16();
-    aParentInfo->mRouterId       = Mle::Mle::RouterIdFromRloc16(parent->GetRloc16());
-    aParentInfo->mNextHop        = parent->GetNextHop();
-    aParentInfo->mPathCost       = parent->GetCost();
-    aParentInfo->mLinkQualityIn  = parent->GetLinkInfo().GetLinkQuality();
-    aParentInfo->mLinkQualityOut = parent->GetLinkQualityOut();
-    aParentInfo->mAge            = static_cast<uint8_t>(Time::MsecToSec(TimerMilli::GetNow() - parent->GetLastHeard()));
-    aParentInfo->mAllocated      = true;
-    aParentInfo->mLinkEstablished = parent->IsStateValid();
-
-#if !OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-exit:
-#endif
-    return error;
+    return AsCoreType(aInstance).Get<Mle::Mle>().GetParentInfo(AsCoreType(aParentInfo));
 }
 
 otError otThreadGetParentAverageRssi(otInstance *aInstance, int8_t *aParentRssi)
 {
     Error error = kErrorNone;
 
-    OT_ASSERT(aParentRssi != nullptr);
+    AssertPointerIsNotNull(aParentRssi);
 
     *aParentRssi = AsCoreType(aInstance).Get<Mle::MleRouter>().GetParent().GetLinkInfo().GetAverageRss();
 
-    VerifyOrExit(*aParentRssi != OT_RADIO_RSSI_INVALID, error = kErrorFailed);
+    VerifyOrExit(*aParentRssi != Radio::kInvalidRssi, error = kErrorFailed);
 
 exit:
     return error;
@@ -393,16 +362,21 @@
 {
     Error error = kErrorNone;
 
-    OT_ASSERT(aLastRssi != nullptr);
+    AssertPointerIsNotNull(aLastRssi);
 
     *aLastRssi = AsCoreType(aInstance).Get<Mle::MleRouter>().GetParent().GetLinkInfo().GetLastRss();
 
-    VerifyOrExit(*aLastRssi != OT_RADIO_RSSI_INVALID, error = kErrorFailed);
+    VerifyOrExit(*aLastRssi != Radio::kInvalidRssi, error = kErrorFailed);
 
 exit:
     return error;
 }
 
+otError otThreadSearchForBetterParent(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<Mle::Mle>().SearchForBetterParent();
+}
+
 otError otThreadSetEnabled(otInstance *aInstance, bool aEnabled)
 {
     Error error = kErrorNone;
@@ -419,30 +393,24 @@
     return error;
 }
 
-uint16_t otThreadGetVersion(void)
-{
-    return OPENTHREAD_CONFIG_THREAD_VERSION;
-}
+uint16_t otThreadGetVersion(void) { return kThreadVersion; }
 
-bool otThreadIsSingleton(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Mle::MleRouter>().IsSingleton();
-}
+bool otThreadIsSingleton(otInstance *aInstance) { return AsCoreType(aInstance).Get<Mle::MleRouter>().IsSingleton(); }
 
-otError otThreadDiscover(otInstance *             aInstance,
+otError otThreadDiscover(otInstance              *aInstance,
                          uint32_t                 aScanChannels,
                          uint16_t                 aPanId,
                          bool                     aJoiner,
                          bool                     aEnableEui64Filtering,
                          otHandleActiveScanResult aCallback,
-                         void *                   aCallbackContext)
+                         void                    *aCallbackContext)
 {
     return AsCoreType(aInstance).Get<Mle::DiscoverScanner>().Discover(
         Mac::ChannelMask(aScanChannels), aPanId, aJoiner, aEnableEui64Filtering,
         /* aFilterIndexes (use hash of factory EUI64) */ nullptr, aCallback, aCallbackContext);
 }
 
-otError otThreadSetJoinerAdvertisement(otInstance *   aInstance,
+otError otThreadSetJoinerAdvertisement(otInstance    *aInstance,
                                        uint32_t       aOui,
                                        const uint8_t *aAdvData,
                                        uint8_t        aAdvDataLength)
@@ -460,33 +428,29 @@
     return &AsCoreType(aInstance).Get<MeshForwarder>().GetCounters();
 }
 
-void otThreadResetIp6Counters(otInstance *aInstance)
-{
-    AsCoreType(aInstance).Get<MeshForwarder>().ResetCounters();
-}
+void otThreadResetIp6Counters(otInstance *aInstance) { AsCoreType(aInstance).Get<MeshForwarder>().ResetCounters(); }
 
 const otMleCounters *otThreadGetMleCounters(otInstance *aInstance)
 {
     return &AsCoreType(aInstance).Get<Mle::MleRouter>().GetCounters();
 }
 
-void otThreadResetMleCounters(otInstance *aInstance)
-{
-    AsCoreType(aInstance).Get<Mle::MleRouter>().ResetCounters();
-}
+void otThreadResetMleCounters(otInstance *aInstance) { AsCoreType(aInstance).Get<Mle::MleRouter>().ResetCounters(); }
 
-void otThreadRegisterParentResponseCallback(otInstance *                   aInstance,
+#if OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
+void otThreadRegisterParentResponseCallback(otInstance                    *aInstance,
                                             otThreadParentResponseCallback aCallback,
-                                            void *                         aContext)
+                                            void                          *aContext)
 {
     AsCoreType(aInstance).Get<Mle::MleRouter>().RegisterParentResponseStatsCallback(aCallback, aContext);
 }
+#endif
 
 #if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE
-otError otThreadLocateAnycastDestination(otInstance *                   aInstance,
-                                         const otIp6Address *           aAnycastAddress,
+otError otThreadLocateAnycastDestination(otInstance                    *aInstance,
+                                         const otIp6Address            *aAnycastAddress,
                                          otThreadAnycastLocatorCallback aCallback,
-                                         void *                         aContext)
+                                         void                          *aContext)
 {
     return AsCoreType(aInstance).Get<AnycastLocator>().Locate(AsCoreType(aAnycastAddress), aCallback, aContext);
 }
@@ -503,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/api/thread_ftd_api.cpp b/src/core/api/thread_ftd_api.cpp
index e8d3a4b..96fb890 100644
--- a/src/core/api/thread_ftd_api.cpp
+++ b/src/core/api/thread_ftd_api.cpp
@@ -79,6 +79,16 @@
     return AsCoreType(aInstance).Get<Mle::MleRouter>().SetPreferredRouterId(aRouterId);
 }
 
+const otDeviceProperties *otThreadGetDeviceProperties(otInstance *aInstance)
+{
+    return &AsCoreType(aInstance).Get<Mle::MleRouter>().GetDeviceProperties();
+}
+
+void otThreadSetDeviceProperties(otInstance *aInstance, const otDeviceProperties *aDeviceProperties)
+{
+    AsCoreType(aInstance).Get<Mle::MleRouter>().SetDeviceProperties(AsCoreType(aDeviceProperties));
+}
+
 uint8_t otThreadGetLocalLeaderWeight(otInstance *aInstance)
 {
     return AsCoreType(aInstance).Get<Mle::MleRouter>().GetLeaderWeight();
@@ -143,6 +153,16 @@
     AsCoreType(aInstance).Get<Mle::MleRouter>().SetRouterUpgradeThreshold(aThreshold);
 }
 
+uint8_t otThreadGetChildRouterLinks(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<Mle::MleRouter>().GetChildRouterLinks();
+}
+
+otError otThreadSetChildRouterLinks(otInstance *aInstance, uint8_t aChildRouterLinks)
+{
+    return AsCoreType(aInstance).Get<Mle::MleRouter>().SetChildRouterLinks(aChildRouterLinks);
+}
+
 otError otThreadReleaseRouterId(otInstance *aInstance, uint8_t aRouterId)
 {
     Error error = kErrorNone;
@@ -205,27 +225,24 @@
 
 otError otThreadGetChildInfoById(otInstance *aInstance, uint16_t aChildId, otChildInfo *aChildInfo)
 {
-    OT_ASSERT(aChildInfo != nullptr);
-
     return AsCoreType(aInstance).Get<ChildTable>().GetChildInfoById(aChildId, AsCoreType(aChildInfo));
 }
 
 otError otThreadGetChildInfoByIndex(otInstance *aInstance, uint16_t aChildIndex, otChildInfo *aChildInfo)
 {
-    OT_ASSERT(aChildInfo != nullptr);
-
     return AsCoreType(aInstance).Get<ChildTable>().GetChildInfoByIndex(aChildIndex, AsCoreType(aChildInfo));
 }
 
-otError otThreadGetChildNextIp6Address(otInstance *               aInstance,
+otError otThreadGetChildNextIp6Address(otInstance                *aInstance,
                                        uint16_t                   aChildIndex,
                                        otChildIp6AddressIterator *aIterator,
-                                       otIp6Address *             aAddress)
+                                       otIp6Address              *aAddress)
 {
     Error        error = kErrorNone;
     const Child *child;
 
-    OT_ASSERT(aIterator != nullptr && aAddress != nullptr);
+    AssertPointerIsNotNull(aIterator);
+    AssertPointerIsNotNull(aAddress);
 
     child = AsCoreType(aInstance).Get<ChildTable>().GetChildAtIndex(aChildIndex);
     VerifyOrExit(child != nullptr, error = kErrorInvalidArgs);
@@ -258,16 +275,13 @@
 
 otError otThreadGetRouterInfo(otInstance *aInstance, uint16_t aRouterId, otRouterInfo *aRouterInfo)
 {
-    OT_ASSERT(aRouterInfo != nullptr);
-
     return AsCoreType(aInstance).Get<RouterTable>().GetRouterInfo(aRouterId, AsCoreType(aRouterInfo));
 }
 
 otError otThreadGetNextCacheEntry(otInstance *aInstance, otCacheEntryInfo *aEntryInfo, otCacheEntryIterator *aIterator)
 {
-    OT_ASSERT((aIterator != nullptr) && (aEntryInfo != nullptr));
-
-    return AsCoreType(aInstance).Get<AddressResolver>().GetNextCacheEntry(*aEntryInfo, *aIterator);
+    return AsCoreType(aInstance).Get<AddressResolver>().GetNextCacheEntry(AsCoreType(aEntryInfo),
+                                                                          AsCoreType(aIterator));
 }
 
 #if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
@@ -283,10 +297,7 @@
 }
 
 #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
-otPskcRef otThreadGetPskcRef(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<KeyManager>().GetPskcRef();
-}
+otPskcRef otThreadGetPskcRef(otInstance *aInstance) { return AsCoreType(aInstance).Get<KeyManager>().GetPskcRef(); }
 #endif
 
 otError otThreadSetPskc(otInstance *aInstance, const otPskc *aPskc)
@@ -336,17 +347,17 @@
     AsCoreType(aInstance).Get<NeighborTable>().RegisterCallback(aCallback);
 }
 
-void otThreadSetDiscoveryRequestCallback(otInstance *                     aInstance,
+void otThreadSetDiscoveryRequestCallback(otInstance                      *aInstance,
                                          otThreadDiscoveryRequestCallback aCallback,
-                                         void *                           aContext)
+                                         void                            *aContext)
 {
     AsCoreType(aInstance).Get<Mle::MleRouter>().SetDiscoveryRequestCallback(aCallback, aContext);
 }
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-void otThreadSendAddressNotification(otInstance *              aInstance,
-                                     otIp6Address *            aDestination,
-                                     otIp6Address *            aTarget,
+void otThreadSendAddressNotification(otInstance               *aInstance,
+                                     otIp6Address             *aDestination,
+                                     otIp6Address             *aTarget,
                                      otIp6InterfaceIdentifier *aMlIid)
 {
     AsCoreType(aInstance).Get<AddressResolver>().SendAddressQueryResponse(AsCoreType(aTarget), AsCoreType(aMlIid),
@@ -354,8 +365,8 @@
 }
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-otError otThreadSendProactiveBackboneNotification(otInstance *              aInstance,
-                                                  otIp6Address *            aTarget,
+otError otThreadSendProactiveBackboneNotification(otInstance               *aInstance,
+                                                  otIp6Address             *aTarget,
                                                   otIp6InterfaceIdentifier *aMlIid,
                                                   uint32_t                  aTimeSinceLastTransaction)
 {
@@ -378,6 +389,9 @@
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
 void otThreadGetRouterIdRange(otInstance *aInstance, uint8_t *aMinRouterId, uint8_t *aMaxRouterId)
 {
+    AssertPointerIsNotNull(aMinRouterId);
+    AssertPointerIsNotNull(aMaxRouterId);
+
     AsCoreType(aInstance).Get<RouterTable>().GetRouterIdRange(*aMinRouterId, *aMaxRouterId);
 }
 
@@ -387,4 +401,22 @@
 }
 #endif
 
+bool otThreadIsRouterIdAllocated(otInstance *aInstance, uint8_t aRouterId)
+{
+    return AsCoreType(aInstance).Get<RouterTable>().IsAllocated(aRouterId);
+}
+
+void otThreadGetNextHopAndPathCost(otInstance *aInstance,
+                                   uint16_t    aDestRloc16,
+                                   uint16_t   *aNextHopRloc16,
+                                   uint8_t    *aPathCost)
+{
+    uint8_t  pathcost;
+    uint16_t nextHopRloc16;
+
+    AsCoreType(aInstance).Get<RouterTable>().GetNextHopAndPathCost(
+        aDestRloc16, (aNextHopRloc16 != nullptr) ? *aNextHopRloc16 : nextHopRloc16,
+        (aPathCost != nullptr) ? *aPathCost : pathcost);
+}
+
 #endif // OPENTHREAD_FTD
diff --git a/src/core/api/trel_api.cpp b/src/core/api/trel_api.cpp
index 01feb25..29c1563 100644
--- a/src/core/api/trel_api.cpp
+++ b/src/core/api/trel_api.cpp
@@ -43,20 +43,12 @@
 
 using namespace ot;
 
-void otTrelEnable(otInstance *aInstance)
+void otTrelSetEnabled(otInstance *aInstance, bool aEnable)
 {
-    AsCoreType(aInstance).Get<Trel::Interface>().Enable();
+    AsCoreType(aInstance).Get<Trel::Interface>().SetEnabled(aEnable);
 }
 
-void otTrelDisable(otInstance *aInstance)
-{
-    AsCoreType(aInstance).Get<Trel::Interface>().Disable();
-}
-
-bool otTrelIsEnabled(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Trel::Interface>().IsEnabled();
-}
+bool otTrelIsEnabled(otInstance *aInstance) { return AsCoreType(aInstance).Get<Trel::Interface>().IsEnabled(); }
 
 void otTrelInitPeerIterator(otInstance *aInstance, otTrelPeerIterator *aIterator)
 {
diff --git a/src/core/api/udp_api.cpp b/src/core/api/udp_api.cpp
index fad774b..16817b6 100644
--- a/src/core/api/udp_api.cpp
+++ b/src/core/api/udp_api.cpp
@@ -62,7 +62,7 @@
 
 otError otUdpBind(otInstance *aInstance, otUdpSocket *aSocket, const otSockAddr *aSockName, otNetifIdentifier aNetif)
 {
-    return AsCoreType(aInstance).Get<Ip6::Udp>().Bind(AsCoreType(aSocket), AsCoreType(aSockName), aNetif);
+    return AsCoreType(aInstance).Get<Ip6::Udp>().Bind(AsCoreType(aSocket), AsCoreType(aSockName), MapEnum(aNetif));
 }
 
 otError otUdpConnect(otInstance *aInstance, otUdpSocket *aSocket, const otSockAddr *aSockName)
@@ -76,10 +76,7 @@
                                                         AsCoreType(aMessageInfo));
 }
 
-otUdpSocket *otUdpGetSockets(otInstance *aInstance)
-{
-    return AsCoreType(aInstance).Get<Ip6::Udp>().GetUdpSockets();
-}
+otUdpSocket *otUdpGetSockets(otInstance *aInstance) { return AsCoreType(aInstance).Get<Ip6::Udp>().GetUdpSockets(); }
 
 #if OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE
 void otUdpForwardSetForwarder(otInstance *aInstance, otUdpForwarder aForwarder, void *aContext)
@@ -87,16 +84,14 @@
     AsCoreType(aInstance).Get<Ip6::Udp>().SetUdpForwarder(aForwarder, aContext);
 }
 
-void otUdpForwardReceive(otInstance *        aInstance,
-                         otMessage *         aMessage,
+void otUdpForwardReceive(otInstance         *aInstance,
+                         otMessage          *aMessage,
                          uint16_t            aPeerPort,
                          const otIp6Address *aPeerAddr,
                          uint16_t            aSockPort)
 {
     Ip6::MessageInfo messageInfo;
 
-    OT_ASSERT(aMessage != nullptr && aPeerAddr != nullptr);
-
     messageInfo.SetSockAddr(AsCoreType(aInstance).Get<Mle::MleRouter>().GetMeshLocal16());
     messageInfo.SetSockPort(aSockPort);
     messageInfo.SetPeerAddr(AsCoreType(aPeerAddr));
diff --git a/src/core/backbone_router/backbone_tmf.cpp b/src/core/backbone_router/backbone_tmf.cpp
index 293793f..d49e31c 100644
--- a/src/core/backbone_router/backbone_tmf.cpp
+++ b/src/core/backbone_router/backbone_tmf.cpp
@@ -43,17 +43,65 @@
 
 RegisterLogModule("Bbr");
 
+BackboneTmfAgent::BackboneTmfAgent(Instance &aInstance)
+    : Coap::Coap(aInstance)
+{
+    SetInterceptor(&Filter, this);
+    SetResourceHandler(&HandleResource);
+}
+
 Error BackboneTmfAgent::Start(void)
 {
     Error error = kErrorNone;
 
-    SuccessOrExit(error = Coap::Start(kBackboneUdpPort, OT_NETIF_BACKBONE));
+    SuccessOrExit(error = Coap::Start(kBackboneUdpPort, Ip6::kNetifBackbone));
+    LogInfo("Start listening on port %u", kBackboneUdpPort);
     SubscribeMulticast(Get<Local>().GetAllNetworkBackboneRoutersAddress());
 
 exit:
     return error;
 }
 
+bool BackboneTmfAgent::HandleResource(CoapBase               &aCoapBase,
+                                      const char             *aUriPath,
+                                      ot::Coap::Message      &aMessage,
+                                      const Ip6::MessageInfo &aMessageInfo)
+{
+    return static_cast<BackboneTmfAgent &>(aCoapBase).HandleResource(aUriPath, aMessage, aMessageInfo);
+}
+
+bool BackboneTmfAgent::HandleResource(const char             *aUriPath,
+                                      ot::Coap::Message      &aMessage,
+                                      const Ip6::MessageInfo &aMessageInfo)
+{
+    OT_UNUSED_VARIABLE(aMessage);
+    OT_UNUSED_VARIABLE(aMessageInfo);
+
+    bool didHandle = true;
+    Uri  uri       = UriFromPath(aUriPath);
+
+#define Case(kUri, Type)                                     \
+    case kUri:                                               \
+        Get<Type>().HandleTmf<kUri>(aMessage, aMessageInfo); \
+        break
+
+    switch (uri)
+    {
+#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
+        Case(kUriBackboneQuery, Manager);
+        Case(kUriBackboneAnswer, Manager);
+#endif
+
+    default:
+        didHandle = false;
+        break;
+    }
+
+#undef Case
+
+    return didHandle;
+}
+
 Error BackboneTmfAgent::Filter(const ot::Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo, void *aContext)
 {
     OT_UNUSED_VARIABLE(aMessage);
@@ -79,14 +127,14 @@
 
 void BackboneTmfAgent::SubscribeMulticast(const Ip6::Address &aAddress)
 {
-    Error error = mSocket.JoinNetifMulticastGroup(OT_NETIF_BACKBONE, aAddress);
+    Error error = mSocket.JoinNetifMulticastGroup(Ip6::kNetifBackbone, aAddress);
 
     LogError("Backbone TMF subscribes", aAddress, error);
 }
 
 void BackboneTmfAgent::UnsubscribeMulticast(const Ip6::Address &aAddress)
 {
-    Error error = mSocket.LeaveNetifMulticastGroup(OT_NETIF_BACKBONE, aAddress);
+    Error error = mSocket.LeaveNetifMulticastGroup(Ip6::kNetifBackbone, aAddress);
 
     LogError("Backbone TMF unsubscribes", aAddress, error);
 }
diff --git a/src/core/backbone_router/backbone_tmf.hpp b/src/core/backbone_router/backbone_tmf.hpp
index 0720f05..73b92ad 100644
--- a/src/core/backbone_router/backbone_tmf.hpp
+++ b/src/core/backbone_router/backbone_tmf.hpp
@@ -39,6 +39,7 @@
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
 
 #include "coap/coap.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 namespace BackboneRouter {
@@ -58,11 +59,7 @@
      * @param[in] aInstance      A reference to the OpenThread instance.
      *
      */
-    explicit BackboneTmfAgent(Instance &aInstance)
-        : Coap::Coap(aInstance)
-    {
-        SetInterceptor(&Filter, this);
-    }
+    explicit BackboneTmfAgent(Instance &aInstance);
 
     /**
      * This method starts the Backbone TMF agent.
@@ -99,7 +96,12 @@
     void UnsubscribeMulticast(const Ip6::Address &aAddress);
 
 private:
-    void         LogError(const char *aText, const Ip6::Address &aAddress, Error aError) const;
+    static bool HandleResource(CoapBase               &aCoapBase,
+                               const char             *aUriPath,
+                               ot::Coap::Message      &aMessage,
+                               const Ip6::MessageInfo &aMessageInfo);
+    bool        HandleResource(const char *aUriPath, ot::Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    void        LogError(const char *aText, const Ip6::Address &aAddress, Error aError) const;
     static Error Filter(const ot::Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo, void *aContext);
 };
 
diff --git a/src/core/backbone_router/bbr_leader.cpp b/src/core/backbone_router/bbr_leader.cpp
index 4b64fff..30330de 100644
--- a/src/core/backbone_router/bbr_leader.cpp
+++ b/src/core/backbone_router/bbr_leader.cpp
@@ -58,7 +58,7 @@
     mDomainPrefix.SetLength(0);
 }
 
-Error Leader::GetConfig(BackboneRouterConfig &aConfig) const
+Error Leader::GetConfig(Config &aConfig) const
 {
     Error error = kErrorNone;
 
@@ -84,7 +84,7 @@
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
-void Leader::LogBackboneRouterPrimary(State aState, const BackboneRouterConfig &aConfig) const
+void Leader::LogBackboneRouterPrimary(State aState, const Config &aConfig) const
 {
     OT_UNUSED_VARIABLE(aConfig);
 
@@ -92,8 +92,8 @@
 
     if (aState != kStateRemoved && aState != kStateNone)
     {
-        LogInfo("Rloc16: 0x%4X, seqno: %d, delay: %d, timeout %d", aConfig.mServer16, aConfig.mSequenceNumber,
-                aConfig.mReregistrationDelay, aConfig.mMlrTimeout);
+        LogInfo("Rloc16:0x%4x, seqno:%u, delay:%u, timeout:%lu", aConfig.mServer16, aConfig.mSequenceNumber,
+                aConfig.mReregistrationDelay, ToUlong(aConfig.mMlrTimeout));
     }
 }
 
@@ -152,9 +152,9 @@
 
 void Leader::UpdateBackboneRouterPrimary(void)
 {
-    BackboneRouterConfig config;
-    State                state;
-    uint32_t             origMlrTimeout;
+    Config   config;
+    State    state;
+    uint32_t origMlrTimeout;
 
     Get<NetworkData::Service::Manager>().GetBackboneRouterPrimary(config);
 
@@ -205,7 +205,8 @@
 
         if (config.mMlrTimeout != origMlrTimeout)
         {
-            LogNote("Leader MLR Timeout is normalized from %u to %u", origMlrTimeout, config.mMlrTimeout);
+            LogNote("Leader MLR Timeout is normalized from %lu to %lu", ToUlong(origMlrTimeout),
+                    ToUlong(config.mMlrTimeout));
         }
     }
 
diff --git a/src/core/backbone_router/bbr_leader.hpp b/src/core/backbone_router/bbr_leader.hpp
index 6266d02..6052839 100644
--- a/src/core/backbone_router/bbr_leader.hpp
+++ b/src/core/backbone_router/bbr_leader.hpp
@@ -52,7 +52,7 @@
 
 namespace BackboneRouter {
 
-typedef otBackboneRouterConfig BackboneRouterConfig;
+typedef otBackboneRouterConfig Config;
 
 /**
  * This class implements the basic Primary Backbone Router service operations.
@@ -112,7 +112,7 @@
      * @retval kErrorNotFound      No Backbone Router in the Thread Network.
      *
      */
-    Error GetConfig(BackboneRouterConfig &aConfig) const;
+    Error GetConfig(Config &aConfig) const;
 
     /**
      * This method gets the Backbone Router Service ID.
@@ -177,17 +177,17 @@
     void UpdateBackboneRouterPrimary(void);
     void UpdateDomainPrefixConfig(void);
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
-    void               LogBackboneRouterPrimary(State aState, const BackboneRouterConfig &aConfig) const;
+    void               LogBackboneRouterPrimary(State aState, const Config &aConfig) const;
     void               LogDomainPrefix(DomainPrefixState aState, const Ip6::Prefix &aPrefix) const;
     static const char *StateToString(State aState);
     static const char *DomainPrefixStateToString(DomainPrefixState aState);
 #else
-    void LogBackboneRouterPrimary(State, const BackboneRouterConfig &) const {}
+    void LogBackboneRouterPrimary(State, const Config &) const {}
     void LogDomainPrefix(DomainPrefixState, const Ip6::Prefix &) const {}
 #endif
 
-    BackboneRouterConfig mConfig;       ///< Primary Backbone Router information.
-    Ip6::Prefix          mDomainPrefix; ///< Domain Prefix in the Thread network.
+    Config      mConfig;       ///< Primary Backbone Router information.
+    Ip6::Prefix mDomainPrefix; ///< Domain Prefix in the Thread network.
 };
 
 } // namespace BackboneRouter
diff --git a/src/core/backbone_router/bbr_local.cpp b/src/core/backbone_router/bbr_local.cpp
index 4c7945a..ee20b2b 100644
--- a/src/core/backbone_router/bbr_local.cpp
+++ b/src/core/backbone_router/bbr_local.cpp
@@ -51,14 +51,12 @@
 
 Local::Local(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mState(OT_BACKBONE_ROUTER_STATE_DISABLED)
+    , mState(kStateDisabled)
     , mMlrTimeout(Mle::kMlrTimeoutDefault)
     , mReregistrationDelay(Mle::kRegistrationDelayDefault)
     , mSequenceNumber(Random::NonCrypto::GetUint8() % 127)
     , mRegistrationJitter(Mle::kBackboneRouterRegistrationJitter)
     , mIsServiceAdded(false)
-    , mDomainPrefixCallback(nullptr)
-    , mDomainPrefixCallbackContext(nullptr)
 {
     mDomainPrefixConfig.GetPrefix().SetLength(0);
 
@@ -83,11 +81,11 @@
 
 void Local::SetEnabled(bool aEnable)
 {
-    VerifyOrExit(aEnable == (mState == OT_BACKBONE_ROUTER_STATE_DISABLED));
+    VerifyOrExit(aEnable != IsEnabled());
 
     if (aEnable)
     {
-        SetState(OT_BACKBONE_ROUTER_STATE_SECONDARY);
+        SetState(kStateSecondary);
         AddDomainPrefixToNetworkData();
         IgnoreError(AddService());
     }
@@ -95,7 +93,7 @@
     {
         RemoveDomainPrefixFromNetworkData();
         RemoveService();
-        SetState(OT_BACKBONE_ROUTER_STATE_DISABLED);
+        SetState(kStateDisabled);
     }
 
 exit:
@@ -104,30 +102,30 @@
 
 void Local::Reset(void)
 {
-    VerifyOrExit(mState != OT_BACKBONE_ROUTER_STATE_DISABLED);
+    VerifyOrExit(mState != kStateDisabled);
 
     RemoveService();
 
-    if (mState == OT_BACKBONE_ROUTER_STATE_PRIMARY)
+    if (mState == kStatePrimary)
     {
         // Increase sequence number when changing from Primary to Secondary.
         SequenceNumberIncrease();
         Get<Notifier>().Signal(kEventThreadBackboneRouterLocalChanged);
-        SetState(OT_BACKBONE_ROUTER_STATE_SECONDARY);
+        SetState(kStateSecondary);
     }
 
 exit:
     return;
 }
 
-void Local::GetConfig(BackboneRouterConfig &aConfig) const
+void Local::GetConfig(Config &aConfig) const
 {
     aConfig.mSequenceNumber      = mSequenceNumber;
     aConfig.mReregistrationDelay = mReregistrationDelay;
     aConfig.mMlrTimeout          = mMlrTimeout;
 }
 
-Error Local::SetConfig(const BackboneRouterConfig &aConfig)
+Error Local::SetConfig(const Config &aConfig)
 {
     Error error  = kErrorNone;
     bool  update = false;
@@ -178,7 +176,7 @@
     Error                                            error = kErrorInvalidState;
     NetworkData::Service::BackboneRouter::ServerData serverData;
 
-    VerifyOrExit(mState != OT_BACKBONE_ROUTER_STATE_DISABLED && Get<Mle::Mle>().IsAttached());
+    VerifyOrExit(mState != kStateDisabled && Get<Mle::Mle>().IsAttached());
 
     VerifyOrExit(aForce /* if register by force */ ||
                  !Get<BackboneRouter::Leader>().HasPrimary() /* if no available Backbone Router service */ ||
@@ -212,21 +210,21 @@
     LogBackboneRouterService("Remove", error);
 }
 
-void Local::SetState(BackboneRouterState aState)
+void Local::SetState(State aState)
 {
     VerifyOrExit(mState != aState);
 
-    if (mState == OT_BACKBONE_ROUTER_STATE_DISABLED)
+    if (mState == kStateDisabled)
     {
         // Update All Network Backbone Routers Multicast Address for both Secondary and Primary state.
         mAllNetworkBackboneRouters.SetMulticastNetworkPrefix(Get<Mle::MleRouter>().GetMeshLocalPrefix());
     }
 
-    if (mState == OT_BACKBONE_ROUTER_STATE_PRIMARY)
+    if (mState == kStatePrimary)
     {
         Get<ThreadNetif>().RemoveUnicastAddress(mBackboneRouterPrimaryAloc);
     }
-    else if (aState == OT_BACKBONE_ROUTER_STATE_PRIMARY)
+    else if (aState == kStatePrimary)
     {
         // Add Primary Backbone Router Aloc for Primary Backbone Router.
         mBackboneRouterPrimaryAloc.GetAddress().SetPrefix(Get<Mle::MleRouter>().GetMeshLocalPrefix());
@@ -241,11 +239,11 @@
     return;
 }
 
-void Local::HandleBackboneRouterPrimaryUpdate(Leader::State aState, const BackboneRouterConfig &aConfig)
+void Local::HandleBackboneRouterPrimaryUpdate(Leader::State aState, const Config &aConfig)
 {
     OT_UNUSED_VARIABLE(aState);
 
-    VerifyOrExit(mState != OT_BACKBONE_ROUTER_STATE_DISABLED && Get<Mle::MleRouter>().IsAttached());
+    VerifyOrExit(IsEnabled() && Get<Mle::MleRouter>().IsAttached());
 
     // Wait some jitter before trying to Register.
     if (aConfig.mServer16 == Mac::kShortAddrInvalid)
@@ -281,7 +279,7 @@
     }
     else
     {
-        SetState(OT_BACKBONE_ROUTER_STATE_PRIMARY);
+        SetState(kStatePrimary);
     }
 
 exit:
@@ -378,21 +376,18 @@
         Get<BackboneTmfAgent>().SubscribeMulticast(mAllDomainBackboneRouters);
     }
 
-    if (mDomainPrefixCallback != nullptr)
+    if (mDomainPrefixCallback.IsSet())
     {
         switch (aState)
         {
         case Leader::kDomainPrefixAdded:
-            mDomainPrefixCallback(mDomainPrefixCallbackContext, OT_BACKBONE_ROUTER_DOMAIN_PREFIX_ADDED,
-                                  Get<Leader>().GetDomainPrefix());
+            mDomainPrefixCallback.Invoke(OT_BACKBONE_ROUTER_DOMAIN_PREFIX_ADDED, Get<Leader>().GetDomainPrefix());
             break;
         case Leader::kDomainPrefixRemoved:
-            mDomainPrefixCallback(mDomainPrefixCallbackContext, OT_BACKBONE_ROUTER_DOMAIN_PREFIX_REMOVED,
-                                  Get<Leader>().GetDomainPrefix());
+            mDomainPrefixCallback.Invoke(OT_BACKBONE_ROUTER_DOMAIN_PREFIX_REMOVED, Get<Leader>().GetDomainPrefix());
             break;
         case Leader::kDomainPrefixRefreshed:
-            mDomainPrefixCallback(mDomainPrefixCallbackContext, OT_BACKBONE_ROUTER_DOMAIN_PREFIX_CHANGED,
-                                  Get<Leader>().GetDomainPrefix());
+            mDomainPrefixCallback.Invoke(OT_BACKBONE_ROUTER_DOMAIN_PREFIX_CHANGED, Get<Leader>().GetDomainPrefix());
             break;
         default:
             break;
@@ -454,17 +449,11 @@
 
 void Local::LogBackboneRouterService(const char *aAction, Error aError)
 {
-    LogInfo("%s BBR Service: seqno (%d), delay (%ds), timeout (%ds), %s", aAction, mSequenceNumber,
-            mReregistrationDelay, mMlrTimeout, ErrorToString(aError));
+    LogInfo("%s BBR Service: seqno (%u), delay (%us), timeout (%lus), %s", aAction, mSequenceNumber,
+            mReregistrationDelay, ToUlong(mMlrTimeout), ErrorToString(aError));
 }
 #endif
 
-void Local::SetDomainPrefixCallback(otBackboneRouterDomainPrefixCallback aCallback, void *aContext)
-{
-    mDomainPrefixCallback        = aCallback;
-    mDomainPrefixCallbackContext = aContext;
-}
-
 } // namespace BackboneRouter
 
 } // namespace ot
diff --git a/src/core/backbone_router/bbr_local.hpp b/src/core/backbone_router/bbr_local.hpp
index b7a76f3..42b1dd1 100644
--- a/src/core/backbone_router/bbr_local.hpp
+++ b/src/core/backbone_router/bbr_local.hpp
@@ -54,6 +54,8 @@
 #include <openthread/backbone_router_ftd.h>
 
 #include "backbone_router/bbr_leader.hpp"
+#include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/log.hpp"
 #include "common/non_copyable.hpp"
@@ -71,7 +73,16 @@
 class Local : public InstanceLocator, private NonCopyable
 {
 public:
-    typedef otBackboneRouterState BackboneRouterState;
+    /**
+     * This enumeration represents Backbone Router state.
+     *
+     */
+    enum State : uint8_t
+    {
+        kStateDisabled  = OT_BACKBONE_ROUTER_STATE_DISABLED,  ///< Backbone function is disabled.
+        kStateSecondary = OT_BACKBONE_ROUTER_STATE_SECONDARY, ///< Secondary Backbone Router.
+        kStatePrimary   = OT_BACKBONE_ROUTER_STATE_PRIMARY,   ///< The Primary Backbone Router.
+    };
 
     /**
      * This constructor initializes the local Backbone Router.
@@ -93,12 +104,10 @@
      * This method retrieves the Backbone Router state.
      *
      *
-     * @retval OT_BACKBONE_ROUTER_STATE_DISABLED   Backbone function is disabled.
-     * @retval OT_BACKBONE_ROUTER_STATE_SECONDARY  Secondary Backbone Router.
-     * @retval OT_BACKBONE_ROUTER_STATE_PRIMARY    Primary Backbone Router.
+     * @returns The current state of Backbone Router.
      *
      */
-    BackboneRouterState GetState(void) const { return mState; }
+    State GetState(void) const { return mState; }
 
     /**
      * This method resets the local Thread Network Data.
@@ -112,7 +121,7 @@
      * @param[out]  aConfig  The local Backbone Router configuration.
      *
      */
-    void GetConfig(BackboneRouterConfig &aConfig) const;
+    void GetConfig(Config &aConfig) const;
 
     /**
      * This method sets local Backbone Router configuration.
@@ -123,13 +132,13 @@
      * @retval kErrorInvalidArgs  The configuration in @p aConfig is invalid.
      *
      */
-    Error SetConfig(const BackboneRouterConfig &aConfig);
+    Error SetConfig(const Config &aConfig);
 
     /**
      * This method registers Backbone Router Dataset to Leader.
      *
-     * @param[in]  aForce True to force registration regardless of current BackboneRouterState.
-     *                    False to decide based on current BackboneRouterState.
+     * @param[in]  aForce True to force registration regardless of current state.
+     *                    False to decide based on current state.
      *
      *
      * @retval kErrorNone            Successfully added the Service entry.
@@ -146,7 +155,7 @@
      * @retval  False if the Backbone Router is not Primary.
      *
      */
-    bool IsPrimary(void) const { return mState == OT_BACKBONE_ROUTER_STATE_PRIMARY; }
+    bool IsPrimary(void) const { return mState == kStatePrimary; }
 
     /**
      * This method indicates whether or not the Backbone Router is enabled.
@@ -155,7 +164,7 @@
      * @retval  False if the Backbone Router is not enabled.
      *
      */
-    bool IsEnabled(void) const { return mState != OT_BACKBONE_ROUTER_STATE_DISABLED; }
+    bool IsEnabled(void) const { return mState != kStateDisabled; }
 
     /**
      * This method sets the Backbone Router registration jitter value.
@@ -180,7 +189,7 @@
      * @param[in]  aConfig  The Primary Backbone Router service.
      *
      */
-    void HandleBackboneRouterPrimaryUpdate(Leader::State aState, const BackboneRouterConfig &aConfig);
+    void HandleBackboneRouterPrimaryUpdate(Leader::State aState, const Config &aConfig);
 
     /**
      * This method gets the Domain Prefix configuration.
@@ -253,10 +262,13 @@
      * @param[in] aContext   A user context pointer.
      *
      */
-    void SetDomainPrefixCallback(otBackboneRouterDomainPrefixCallback aCallback, void *aContext);
+    void SetDomainPrefixCallback(otBackboneRouterDomainPrefixCallback aCallback, void *aContext)
+    {
+        mDomainPrefixCallback.Set(aCallback, aContext);
+    }
 
 private:
-    void SetState(BackboneRouterState aState);
+    void SetState(State aState);
     void RemoveService(void);
     void AddDomainPrefixToNetworkData(void);
     void RemoveDomainPrefixFromNetworkData(void);
@@ -269,11 +281,11 @@
     void LogDomainPrefix(const char *, Error) {}
 #endif
 
-    BackboneRouterState mState;
-    uint32_t            mMlrTimeout;
-    uint16_t            mReregistrationDelay;
-    uint8_t             mSequenceNumber;
-    uint8_t             mRegistrationJitter;
+    State    mState;
+    uint32_t mMlrTimeout;
+    uint16_t mReregistrationDelay;
+    uint8_t  mSequenceNumber;
+    uint8_t  mRegistrationJitter;
 
     // Indicates whether or not already add Backbone Router Service to local server data.
     // Used to check whether or not in restore stage after reset or whether to remove
@@ -282,15 +294,16 @@
 
     NetworkData::OnMeshPrefixConfig mDomainPrefixConfig;
 
-    Ip6::Netif::UnicastAddress           mBackboneRouterPrimaryAloc;
-    Ip6::Address                         mAllNetworkBackboneRouters;
-    Ip6::Address                         mAllDomainBackboneRouters;
-    otBackboneRouterDomainPrefixCallback mDomainPrefixCallback;
-    void *                               mDomainPrefixCallbackContext;
+    Ip6::Netif::UnicastAddress                     mBackboneRouterPrimaryAloc;
+    Ip6::Address                                   mAllNetworkBackboneRouters;
+    Ip6::Address                                   mAllDomainBackboneRouters;
+    Callback<otBackboneRouterDomainPrefixCallback> mDomainPrefixCallback;
 };
 
 } // namespace BackboneRouter
 
+DefineMapEnum(otBackboneRouterState, BackboneRouter::Local::State);
+
 /**
  * @}
  */
diff --git a/src/core/backbone_router/bbr_manager.cpp b/src/core/backbone_router/bbr_manager.cpp
index 2de2f6f..fd04b12 100644
--- a/src/core/backbone_router/bbr_manager.cpp
+++ b/src/core/backbone_router/bbr_manager.cpp
@@ -40,6 +40,7 @@
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 #include "thread/mle_types.hpp"
 #include "thread/thread_netif.hpp"
@@ -54,19 +55,13 @@
 
 Manager::Manager(Instance &aInstance)
     : InstanceLocator(aInstance)
-#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
-    , mMulticastListenerRegistration(UriPath::kMlr, Manager::HandleMulticastListenerRegistration, this)
-#endif
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-    , mDuaRegistration(UriPath::kDuaRegistrationRequest, Manager::HandleDuaRegistration, this)
-    , mBackboneQuery(UriPath::kBackboneQuery, Manager::HandleBackboneQuery, this)
-    , mBackboneAnswer(UriPath::kBackboneAnswer, Manager::HandleBackboneAnswer, this)
     , mNdProxyTable(aInstance)
 #endif
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
     , mMulticastListenersTable(aInstance)
 #endif
-    , mTimer(aInstance, Manager::HandleTimer)
+    , mTimer(aInstance)
     , mBackboneTmfAgent(aInstance)
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
@@ -83,10 +78,6 @@
 #endif
 #endif
 {
-#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-    mBackboneTmfAgent.AddResource(mBackboneQuery);
-    mBackboneTmfAgent.AddResource(mBackboneAnswer);
-#endif
 }
 
 void Manager::HandleNotifierEvents(Events aEvents)
@@ -95,15 +86,11 @@
 
     if (aEvents.Contains(kEventThreadBackboneRouterStateChanged))
     {
-        if (Get<Local>().GetState() == OT_BACKBONE_ROUTER_STATE_DISABLED)
+        if (!Get<Local>().IsEnabled())
         {
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
-            Get<Tmf::Agent>().RemoveResource(mMulticastListenerRegistration);
             mMulticastListenersTable.Clear();
 #endif
-#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-            Get<Tmf::Agent>().RemoveResource(mDuaRegistration);
-#endif
             mTimer.Stop();
 
             error = mBackboneTmfAgent.Stop();
@@ -119,12 +106,6 @@
         }
         else
         {
-#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
-            Get<Tmf::Agent>().AddResource(mMulticastListenerRegistration);
-#endif
-#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-            Get<Tmf::Agent>().AddResource(mDuaRegistration);
-#endif
             if (!mTimer.IsRunning())
             {
                 mTimer.Start(kTimerInterval);
@@ -137,11 +118,6 @@
     }
 }
 
-void Manager::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Manager>().HandleTimer();
-}
-
 void Manager::HandleTimer(void)
 {
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
@@ -156,12 +132,21 @@
 }
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
+template <> void Manager::HandleTmf<kUriMlr>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    VerifyOrExit(Get<Local>().IsEnabled());
+    HandleMulticastListenerRegistration(aMessage, aMessageInfo);
+
+exit:
+    return;
+}
+
 void Manager::HandleMulticastListenerRegistration(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error                      error     = kErrorNone;
     bool                       isPrimary = Get<Local>().IsPrimary();
     ThreadStatusTlv::MlrStatus status    = ThreadStatusTlv::kMlrSuccess;
-    BackboneRouterConfig       config;
+    Config                     config;
 
     uint16_t     addressesOffset, addressesLength;
     Ip6::Address address;
@@ -224,11 +209,11 @@
         {
             uint32_t origTimeout = timeout;
 
-            timeout = OT_MIN(timeout, static_cast<uint32_t>(Mle::kMlrTimeoutMax));
+            timeout = Min(timeout, Mle::kMlrTimeoutMax);
 
             if (timeout != origTimeout)
             {
-                LogNote("MLR.req: MLR timeout is normalized from %u to %u", origTimeout, timeout);
+                LogNote("MLR.req: MLR timeout is normalized from %lu to %lu", ToUlong(origTimeout), ToUlong(timeout));
             }
         }
     }
@@ -296,10 +281,10 @@
     }
 }
 
-void Manager::SendMulticastListenerRegistrationResponse(const Coap::Message &      aMessage,
-                                                        const Ip6::MessageInfo &   aMessageInfo,
+void Manager::SendMulticastListenerRegistrationResponse(const Coap::Message       &aMessage,
+                                                        const Ip6::MessageInfo    &aMessageInfo,
                                                         ThreadStatusTlv::MlrStatus aStatus,
-                                                        Ip6::Address *             aFailedAddresses,
+                                                        Ip6::Address              *aFailedAddresses,
                                                         uint8_t                    aFailedAddressNum)
 {
     Error          error = kErrorNone;
@@ -336,14 +321,14 @@
                                                         uint32_t            aTimeout)
 {
     Error             error   = kErrorNone;
-    Coap::Message *   message = nullptr;
+    Coap::Message    *message = nullptr;
     Ip6::MessageInfo  messageInfo;
     Ip6AddressesTlv   addressesTlv;
     BackboneTmfAgent &backboneTmf = Get<BackboneRouter::BackboneTmfAgent>();
 
     OT_ASSERT(aAddressNum >= Ip6AddressesTlv::kMinAddresses && aAddressNum <= Ip6AddressesTlv::kMaxAddresses);
 
-    message = backboneTmf.NewNonConfirmablePostMessage(UriPath::kBackboneMlr);
+    message = backboneTmf.NewNonConfirmablePostMessage(kUriBackboneMlr);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     addressesTlv.Init();
@@ -368,6 +353,17 @@
 #endif // OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
+
+template <>
+void Manager::HandleTmf<kUriDuaRegistrationRequest>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    VerifyOrExit(Get<Local>().IsEnabled());
+    HandleDuaRegistration(aMessage, aMessageInfo);
+
+exit:
+    return;
+}
+
 void Manager::HandleDuaRegistration(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error                      error     = kErrorNone;
@@ -445,9 +441,9 @@
     }
 }
 
-void Manager::SendDuaRegistrationResponse(const Coap::Message &      aMessage,
-                                          const Ip6::MessageInfo &   aMessageInfo,
-                                          const Ip6::Address &       aTarget,
+void Manager::SendDuaRegistrationResponse(const Coap::Message       &aMessage,
+                                          const Ip6::MessageInfo    &aMessageInfo,
+                                          const Ip6::Address        &aTarget,
                                           ThreadStatusTlv::DuaStatus aStatus)
 {
     Error          error = kErrorNone;
@@ -496,10 +492,7 @@
 #endif
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-NdProxyTable &Manager::GetNdProxyTable(void)
-{
-    return mNdProxyTable;
-}
+NdProxyTable &Manager::GetNdProxyTable(void) { return mNdProxyTable; }
 
 bool Manager::ShouldForwardDuaToBackbone(const Ip6::Address &aAddress)
 {
@@ -524,12 +517,12 @@
 Error Manager::SendBackboneQuery(const Ip6::Address &aDua, uint16_t aRloc16)
 {
     Error            error   = kErrorNone;
-    Coap::Message *  message = nullptr;
+    Coap::Message   *message = nullptr;
     Ip6::MessageInfo messageInfo;
 
     VerifyOrExit(Get<Local>().IsPrimary(), error = kErrorInvalidState);
 
-    message = mBackboneTmfAgent.NewPriorityNonConfirmablePostMessage(UriPath::kBackboneQuery);
+    message = mBackboneTmfAgent.NewPriorityNonConfirmablePostMessage(kUriBackboneQuery);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = Tlv::Append<ThreadTargetTlv>(*message, aDua));
@@ -553,12 +546,7 @@
     return error;
 }
 
-void Manager::HandleBackboneQuery(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<Manager *>(aContext)->HandleBackboneQuery(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void Manager::HandleBackboneQuery(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void Manager::HandleTmf<kUriBackboneQuery>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error                  error = kErrorNone;
     Ip6::Address           dua;
@@ -587,12 +575,7 @@
     LogInfo("HandleBackboneQuery: %s", ErrorToString(error));
 }
 
-void Manager::HandleBackboneAnswer(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<Manager *>(aContext)->HandleBackboneAnswer(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void Manager::HandleBackboneAnswer(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void Manager::HandleTmf<kUriBackboneAnswer>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error                    error = kErrorNone;
     bool                     proactive;
@@ -638,7 +621,7 @@
     LogInfo("HandleBackboneAnswer: %s", ErrorToString(error));
 }
 
-Error Manager::SendProactiveBackboneNotification(const Ip6::Address &            aDua,
+Error Manager::SendProactiveBackboneNotification(const Ip6::Address             &aDua,
                                                  const Ip6::InterfaceIdentifier &aMeshLocalIid,
                                                  uint32_t                        aTimeSinceLastTransaction)
 {
@@ -646,8 +629,8 @@
                               aTimeSinceLastTransaction, Mac::kShortAddrInvalid);
 }
 
-Error Manager::SendBackboneAnswer(const Ip6::MessageInfo &     aQueryMessageInfo,
-                                  const Ip6::Address &         aDua,
+Error Manager::SendBackboneAnswer(const Ip6::MessageInfo      &aQueryMessageInfo,
+                                  const Ip6::Address          &aDua,
                                   uint16_t                     aSrcRloc16,
                                   const NdProxyTable::NdProxy &aNdProxy)
 {
@@ -655,21 +638,21 @@
                               aNdProxy.GetTimeSinceLastTransaction(), aSrcRloc16);
 }
 
-Error Manager::SendBackboneAnswer(const Ip6::Address &            aDstAddr,
-                                  const Ip6::Address &            aDua,
+Error Manager::SendBackboneAnswer(const Ip6::Address             &aDstAddr,
+                                  const Ip6::Address             &aDua,
                                   const Ip6::InterfaceIdentifier &aMeshLocalIid,
                                   uint32_t                        aTimeSinceLastTransaction,
                                   uint16_t                        aSrcRloc16)
 {
     Error            error   = kErrorNone;
-    Coap::Message *  message = nullptr;
+    Coap::Message   *message = nullptr;
     Ip6::MessageInfo messageInfo;
     bool             proactive = aDstAddr.IsMulticast();
 
     VerifyOrExit((message = mBackboneTmfAgent.NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = message->Init(proactive ? Coap::kTypeNonConfirmable : Coap::kTypeConfirmable, Coap::kCodePost,
-                                        UriPath::kBackboneAnswer));
+                                        kUriBackboneAnswer));
     SuccessOrExit(error = message->SetPayloadMarker());
 
     SuccessOrExit(error = Tlv::Append<ThreadTargetTlv>(*message, aDua));
@@ -678,11 +661,8 @@
 
     SuccessOrExit(error = Tlv::Append<ThreadLastTransactionTimeTlv>(*message, aTimeSinceLastTransaction));
 
-    {
-        const MeshCoP::NameData nameData = Get<MeshCoP::NetworkNameManager>().GetNetworkName().GetAsData();
-
-        SuccessOrExit(error = Tlv::Append<ThreadNetworkNameTlv>(*message, nameData.GetBuffer(), nameData.GetLength()));
-    }
+    SuccessOrExit(error = Tlv::Append<ThreadNetworkNameTlv>(
+                      *message, Get<MeshCoP::NetworkNameManager>().GetNetworkName().GetAsCString()));
 
     if (aSrcRloc16 != Mac::kShortAddrInvalid)
     {
@@ -732,7 +712,7 @@
             aDua.ToString().AsCString(), aMeshLocalIid.ToString().AsCString(), duplicate ? "Y" : "N");
 }
 
-void Manager::HandleExtendedBackboneAnswer(const Ip6::Address &            aDua,
+void Manager::HandleExtendedBackboneAnswer(const Ip6::Address             &aDua,
                                            const Ip6::InterfaceIdentifier &aMeshLocalIid,
                                            uint32_t                        aTimeSinceLastTransaction,
                                            uint16_t                        aSrcRloc16)
@@ -742,11 +722,11 @@
     dest.SetToRoutingLocator(Get<Mle::MleRouter>().GetMeshLocalPrefix(), aSrcRloc16);
     Get<AddressResolver>().SendAddressQueryResponse(aDua, aMeshLocalIid, &aTimeSinceLastTransaction, dest);
 
-    LogInfo("HandleExtendedBackboneAnswer: target=%s, mliid=%s, LTT=%lds, rloc16=%04x", aDua.ToString().AsCString(),
-            aMeshLocalIid.ToString().AsCString(), aTimeSinceLastTransaction, aSrcRloc16);
+    LogInfo("HandleExtendedBackboneAnswer: target=%s, mliid=%s, LTT=%lus, rloc16=%04x", aDua.ToString().AsCString(),
+            aMeshLocalIid.ToString().AsCString(), ToUlong(aTimeSinceLastTransaction), aSrcRloc16);
 }
 
-void Manager::HandleProactiveBackboneNotification(const Ip6::Address &            aDua,
+void Manager::HandleProactiveBackboneNotification(const Ip6::Address             &aDua,
                                                   const Ip6::InterfaceIdentifier &aMeshLocalIid,
                                                   uint32_t                        aTimeSinceLastTransaction)
 {
@@ -779,8 +759,8 @@
     }
 
 exit:
-    LogInfo("HandleProactiveBackboneNotification: %s, target=%s, mliid=%s, LTT=%lds", ErrorToString(error),
-            aDua.ToString().AsCString(), aMeshLocalIid.ToString().AsCString(), aTimeSinceLastTransaction);
+    LogInfo("HandleProactiveBackboneNotification: %s, target=%s, mliid=%s, LTT=%lus", ErrorToString(error),
+            aDua.ToString().AsCString(), aMeshLocalIid.ToString().AsCString(), ToUlong(aTimeSinceLastTransaction));
 }
 #endif // OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
 
diff --git a/src/core/backbone_router/bbr_manager.hpp b/src/core/backbone_router/bbr_manager.hpp
index e5f1705..da5434d 100644
--- a/src/core/backbone_router/bbr_manager.hpp
+++ b/src/core/backbone_router/bbr_manager.hpp
@@ -49,6 +49,7 @@
 #include "common/non_copyable.hpp"
 #include "net/netif.hpp"
 #include "thread/network_data.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -61,6 +62,8 @@
 class Manager : public InstanceLocator, private NonCopyable
 {
     friend class ot::Notifier;
+    friend class Tmf::Agent;
+    friend class BackboneTmfAgent;
 
 public:
     /**
@@ -164,27 +167,22 @@
      * @retval kErrorNoBufs        If insufficient message buffers available.
      *
      */
-    Error SendProactiveBackboneNotification(const Ip6::Address &            aDua,
+    Error SendProactiveBackboneNotification(const Ip6::Address             &aDua,
                                             const Ip6::InterfaceIdentifier &aMeshLocalIid,
                                             uint32_t                        aTimeSinceLastTransaction);
 
 private:
     static constexpr uint32_t kTimerInterval = 1000;
 
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
-    static void HandleMulticastListenerRegistration(void *               aContext,
-                                                    otMessage *          aMessage,
-                                                    const otMessageInfo *aMessageInfo)
-    {
-        static_cast<Manager *>(aContext)->HandleMulticastListenerRegistration(
-            *static_cast<const Coap::Message *>(aMessage), *static_cast<const Ip6::MessageInfo *>(aMessageInfo));
-    }
     void HandleMulticastListenerRegistration(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    void SendMulticastListenerRegistrationResponse(const Coap::Message &      aMessage,
-                                                   const Ip6::MessageInfo &   aMessageInfo,
+    void SendMulticastListenerRegistrationResponse(const Coap::Message       &aMessage,
+                                                   const Ip6::MessageInfo    &aMessageInfo,
                                                    ThreadStatusTlv::MlrStatus aStatus,
-                                                   Ip6::Address *             aFailedAddresses,
+                                                   Ip6::Address              *aFailedAddresses,
                                                    uint8_t                    aFailedAddressNum);
     void SendBackboneMulticastListenerRegistration(const Ip6::Address *aAddresses,
                                                    uint8_t             aAddressNum,
@@ -192,59 +190,45 @@
 #endif
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-    static void HandleDuaRegistration(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-    {
-        static_cast<Manager *>(aContext)->HandleDuaRegistration(*static_cast<const Coap::Message *>(aMessage),
-                                                                *static_cast<const Ip6::MessageInfo *>(aMessageInfo));
-    }
-    void        HandleDuaRegistration(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    static void HandleBackboneQuery(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleBackboneQuery(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    static void HandleBackboneAnswer(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleBackboneAnswer(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    Error       SendBackboneAnswer(const Ip6::MessageInfo &     aQueryMessageInfo,
-                                   const Ip6::Address &         aDua,
-                                   uint16_t                     aSrcRloc16,
-                                   const NdProxyTable::NdProxy &aNdProxy);
-    Error       SendBackboneAnswer(const Ip6::Address &            aDstAddr,
-                                   const Ip6::Address &            aDua,
-                                   const Ip6::InterfaceIdentifier &aMeshLocalIid,
-                                   uint32_t                        aTimeSinceLastTransaction,
-                                   uint16_t                        aSrcRloc16);
-    void        HandleDadBackboneAnswer(const Ip6::Address &aDua, const Ip6::InterfaceIdentifier &aMeshLocalIid);
-    void        HandleExtendedBackboneAnswer(const Ip6::Address &            aDua,
-                                             const Ip6::InterfaceIdentifier &aMeshLocalIid,
-                                             uint32_t                        aTimeSinceLastTransaction,
-                                             uint16_t                        aSrcRloc16);
-    void        HandleProactiveBackboneNotification(const Ip6::Address &            aDua,
-                                                    const Ip6::InterfaceIdentifier &aMeshLocalIid,
-                                                    uint32_t                        aTimeSinceLastTransaction);
-    void        SendDuaRegistrationResponse(const Coap::Message &      aMessage,
-                                            const Ip6::MessageInfo &   aMessageInfo,
-                                            const Ip6::Address &       aTarget,
-                                            ThreadStatusTlv::DuaStatus aStatus);
+    void  HandleDuaRegistration(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    Error SendBackboneAnswer(const Ip6::MessageInfo      &aQueryMessageInfo,
+                             const Ip6::Address          &aDua,
+                             uint16_t                     aSrcRloc16,
+                             const NdProxyTable::NdProxy &aNdProxy);
+    Error SendBackboneAnswer(const Ip6::Address             &aDstAddr,
+                             const Ip6::Address             &aDua,
+                             const Ip6::InterfaceIdentifier &aMeshLocalIid,
+                             uint32_t                        aTimeSinceLastTransaction,
+                             uint16_t                        aSrcRloc16);
+    void  HandleDadBackboneAnswer(const Ip6::Address &aDua, const Ip6::InterfaceIdentifier &aMeshLocalIid);
+    void  HandleExtendedBackboneAnswer(const Ip6::Address             &aDua,
+                                       const Ip6::InterfaceIdentifier &aMeshLocalIid,
+                                       uint32_t                        aTimeSinceLastTransaction,
+                                       uint16_t                        aSrcRloc16);
+    void  HandleProactiveBackboneNotification(const Ip6::Address             &aDua,
+                                              const Ip6::InterfaceIdentifier &aMeshLocalIid,
+                                              uint32_t                        aTimeSinceLastTransaction);
+    void  SendDuaRegistrationResponse(const Coap::Message       &aMessage,
+                                      const Ip6::MessageInfo    &aMessageInfo,
+                                      const Ip6::Address        &aTarget,
+                                      ThreadStatusTlv::DuaStatus aStatus);
 #endif
     void HandleNotifierEvents(Events aEvents);
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTimer(void);
 
     void LogError(const char *aText, Error aError) const;
 
-#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
-    Coap::Resource mMulticastListenerRegistration;
-#endif
+    using BbrTimer = TimerMilliIn<Manager, &Manager::HandleTimer>;
+
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-    Coap::Resource mDuaRegistration;
-    Coap::Resource mBackboneQuery;
-    Coap::Resource mBackboneAnswer;
-    NdProxyTable   mNdProxyTable;
+    NdProxyTable mNdProxyTable;
 #endif
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
     MulticastListenersTable mMulticastListenersTable;
 #endif
-    TimerMilli mTimer;
+    BbrTimer mTimer;
 
     BackboneTmfAgent mBackboneTmfAgent;
 
@@ -265,6 +249,15 @@
 #endif
 };
 
+#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
+DeclareTmfHandler(Manager, kUriMlr);
+#endif
+#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
+DeclareTmfHandler(Manager, kUriDuaRegistrationRequest);
+DeclareTmfHandler(Manager, kUriBackboneQuery);
+DeclareTmfHandler(Manager, kUriBackboneAnswer);
+#endif
+
 } // namespace BackboneRouter
 
 /**
diff --git a/src/core/backbone_router/multicast_listeners_table.cpp b/src/core/backbone_router/multicast_listeners_table.cpp
index 35a4972..4f477cd 100644
--- a/src/core/backbone_router/multicast_listeners_table.cpp
+++ b/src/core/backbone_router/multicast_listeners_table.cpp
@@ -77,13 +77,10 @@
 
     FixHeap(mNumValidListeners - 1);
 
-    if (mCallback != nullptr)
-    {
-        mCallback(mCallbackContext, OT_BACKBONE_ROUTER_MULTICAST_LISTENER_ADDED, &aAddress);
-    }
+    mCallback.InvokeIfSet(MapEnum(Listener::kEventAdded), &aAddress);
 
 exit:
-    LogMulticastListenersTable("Add", aAddress, aExpireTime, error);
+    Log(kAdd, aAddress, aExpireTime, error);
     CheckInvariants();
     return error;
 }
@@ -106,17 +103,14 @@
                 FixHeap(i);
             }
 
-            if (mCallback != nullptr)
-            {
-                mCallback(mCallbackContext, OT_BACKBONE_ROUTER_MULTICAST_LISTENER_REMOVED, &aAddress);
-            }
+            mCallback.InvokeIfSet(MapEnum(Listener::kEventRemoved), &aAddress);
 
             ExitNow(error = kErrorNone);
         }
     }
 
 exit:
-    LogMulticastListenersTable("Remove", aAddress, TimeMilli(0), error);
+    Log(kRemove, aAddress, TimeMilli(0), error);
     CheckInvariants();
 }
 
@@ -127,7 +121,7 @@
 
     while (mNumValidListeners > 0 && now >= mListeners[0].GetExpireTime())
     {
-        LogMulticastListenersTable("Expire", mListeners[0].GetAddress(), mListeners[0].GetExpireTime(), kErrorNone);
+        Log(kExpire, mListeners[0].GetAddress(), mListeners[0].GetExpireTime(), kErrorNone);
         address = mListeners[0].GetAddress();
 
         mNumValidListeners--;
@@ -138,28 +132,34 @@
             FixHeap(0);
         }
 
-        if (mCallback != nullptr)
-        {
-            mCallback(mCallbackContext, OT_BACKBONE_ROUTER_MULTICAST_LISTENER_REMOVED, &address);
-        }
+        mCallback.InvokeIfSet(MapEnum(Listener::kEventRemoved), &address);
     }
 
     CheckInvariants();
 }
 
-void MulticastListenersTable::LogMulticastListenersTable(const char *        aAction,
-                                                         const Ip6::Address &aAddress,
-                                                         TimeMilli           aExpireTime,
-                                                         Error               aError)
+#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_DEBG)
+void MulticastListenersTable::Log(Action              aAction,
+                                  const Ip6::Address &aAddress,
+                                  TimeMilli           aExpireTime,
+                                  Error               aError) const
 {
-    OT_UNUSED_VARIABLE(aAction);
-    OT_UNUSED_VARIABLE(aAddress);
-    OT_UNUSED_VARIABLE(aExpireTime);
-    OT_UNUSED_VARIABLE(aError);
+    static const char *const kActionStrings[] = {
+        "Add",    // (0) kAdd
+        "Remove", // (1) kRemove
+        "Expire", // (2) kExpire
+    };
 
-    LogDebg("%s %s expire %u: %s", aAction, aAddress.ToString().AsCString(), aExpireTime.GetValue(),
-            ErrorToString(aError));
+    static_assert(0 == kAdd, "kAdd value is incorrect");
+    static_assert(1 == kRemove, "kRemove value is incorrect");
+    static_assert(2 == kExpire, "kExpire value is incorrect");
+
+    LogDebg("%s %s expire %lu: %s", kActionStrings[aAction], aAddress.ToString().AsCString(),
+            ToUlong(aExpireTime.GetValue()), ErrorToString(aError));
 }
+#else
+void MulticastListenersTable::Log(Action, const Ip6::Address &, TimeMilli, Error) const {}
+#endif
 
 void MulticastListenersTable::FixHeap(uint16_t aIndex)
 {
@@ -263,11 +263,11 @@
 
 void MulticastListenersTable::Clear(void)
 {
-    if (mCallback != nullptr)
+    if (mCallback.IsSet())
     {
         for (uint16_t i = 0; i < mNumValidListeners; i++)
         {
-            mCallback(mCallbackContext, OT_BACKBONE_ROUTER_MULTICAST_LISTENER_REMOVED, &mListeners[i].GetAddress());
+            mCallback.Invoke(MapEnum(Listener::kEventRemoved), &mListeners[i].GetAddress());
         }
     }
 
@@ -276,22 +276,20 @@
     CheckInvariants();
 }
 
-void MulticastListenersTable::SetCallback(otBackboneRouterMulticastListenerCallback aCallback, void *aContext)
+void MulticastListenersTable::SetCallback(Listener::Callback aCallback, void *aContext)
 {
-    mCallback        = aCallback;
-    mCallbackContext = aContext;
+    mCallback.Set(aCallback, aContext);
 
-    if (mCallback != nullptr)
+    if (mCallback.IsSet())
     {
         for (uint16_t i = 0; i < mNumValidListeners; i++)
         {
-            mCallback(mCallbackContext, OT_BACKBONE_ROUTER_MULTICAST_LISTENER_ADDED, &mListeners[i].GetAddress());
+            mCallback.Invoke(MapEnum(Listener::kEventAdded), &mListeners[i].GetAddress());
         }
     }
 }
 
-Error MulticastListenersTable::GetNext(otBackboneRouterMulticastListenerIterator &aIterator,
-                                       otBackboneRouterMulticastListenerInfo &    aListenerInfo)
+Error MulticastListenersTable::GetNext(Listener::Iterator &aIterator, Listener::Info &aInfo)
 {
     Error     error = kErrorNone;
     TimeMilli now;
@@ -300,8 +298,8 @@
 
     now = TimerMilli::GetNow();
 
-    aListenerInfo.mAddress = mListeners[aIterator].mAddress;
-    aListenerInfo.mTimeout =
+    aInfo.mAddress = mListeners[aIterator].mAddress;
+    aInfo.mTimeout =
         Time::MsecToSec(mListeners[aIterator].mExpireTime > now ? mListeners[aIterator].mExpireTime - now : 0);
 
     aIterator++;
diff --git a/src/core/backbone_router/multicast_listeners_table.hpp b/src/core/backbone_router/multicast_listeners_table.hpp
index b7ae80c..7565f94 100644
--- a/src/core/backbone_router/multicast_listeners_table.hpp
+++ b/src/core/backbone_router/multicast_listeners_table.hpp
@@ -40,6 +40,8 @@
 
 #include <openthread/backbone_router_ftd.h>
 
+#include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/non_copyable.hpp"
 #include "common/notifier.hpp"
 #include "common/time.hpp"
@@ -67,6 +69,16 @@
         friend class MulticastListenersTable;
 
     public:
+        typedef otBackboneRouterMulticastListenerCallback Callback; ///< Listener callback.
+        typedef otBackboneRouterMulticastListenerIterator Iterator; ///< Iterator to go over Listener entries.
+        typedef otBackboneRouterMulticastListenerInfo     Info;     ///< Listener info.
+
+        enum Event : uint8_t ///< Listener Event
+        {
+            kEventAdded   = OT_BACKBONE_ROUTER_MULTICAST_LISTENER_ADDED,   ///< Listener was added.
+            kEventRemoved = OT_BACKBONE_ROUTER_MULTICAST_LISTENER_REMOVED, ///< Listener was removed.
+        };
+
         /**
          * This constructor initializes the `Listener` object.
          *
@@ -108,8 +120,6 @@
     explicit MulticastListenersTable(Instance &aInstance)
         : InstanceLocator(aInstance)
         , mNumValidListeners(0)
-        , mCallback(nullptr)
-        , mCallbackContext(nullptr)
     {
     }
 
@@ -173,27 +183,24 @@
      * @param[in] aContext   A user context pointer.
      *
      */
-    void SetCallback(otBackboneRouterMulticastListenerCallback aCallback, void *aContext);
+    void SetCallback(Listener::Callback aCallback, void *aContext);
 
     /**
      * This method gets the next Multicast Listener.
      *
-     * @param[in] aIterator       A pointer to the Multicast Listener Iterator.
-     * @param[out] aListenerInfo  A pointer to where the Multicast Listener info is placed.
+     * @param[in]  aIterator      A pointer to the Multicast Listener Iterator.
+     * @param[out] aInfo          A reference to output the Multicast Listener info.
      *
      * @retval kErrorNone         Successfully found the next Multicast Listener info.
      * @retval kErrorNotFound     No subsequent Multicast Listener was found.
      *
      */
-    Error GetNext(otBackboneRouterMulticastListenerIterator &aIterator,
-                  otBackboneRouterMulticastListenerInfo &    aListenerInfo);
+    Error GetNext(Listener::Iterator &aIterator, Listener::Info &aInfo);
 
 private:
-    static constexpr uint16_t kMulticastListenersTableSize = OPENTHREAD_CONFIG_MAX_MULTICAST_LISTENERS;
+    static constexpr uint16_t kTableSize = OPENTHREAD_CONFIG_MAX_MULTICAST_LISTENERS;
 
-    static_assert(
-        kMulticastListenersTableSize >= 75,
-        "Thread 1.2 Conformance requires the Multicast Listener Table size to be larger than or equal to 75.");
+    static_assert(kTableSize >= 75, "Thread 1.2 Conformance requires table size of at least 75 listeners.");
 
     class IteratorBuilder : InstanceLocator
     {
@@ -207,25 +214,29 @@
         Listener *end(void);
     };
 
-    void LogMulticastListenersTable(const char *        aAction,
-                                    const Ip6::Address &aAddress,
-                                    TimeMilli           aExpireTime,
-                                    Error               aError);
+    enum Action : uint8_t
+    {
+        kAdd,
+        kRemove,
+        kExpire,
+    };
+
+    void Log(Action aAction, const Ip6::Address &aAddress, TimeMilli aExpireTime, Error aError) const;
 
     void FixHeap(uint16_t aIndex);
     bool SiftHeapElemDown(uint16_t aIndex);
     void SiftHeapElemUp(uint16_t aIndex);
     void CheckInvariants(void) const;
 
-    Listener mListeners[kMulticastListenersTableSize];
-    uint16_t mNumValidListeners;
-
-    otBackboneRouterMulticastListenerCallback mCallback;
-    void *                                    mCallbackContext;
+    Listener                     mListeners[kTableSize];
+    uint16_t                     mNumValidListeners;
+    Callback<Listener::Callback> mCallback;
 };
 
 } // namespace BackboneRouter
 
+DefineMapEnum(otBackboneRouterMulticastListenerEvent, BackboneRouter::MulticastListenersTable::Listener::Event);
+
 /**
  * @}
  */
diff --git a/src/core/backbone_router/ndproxy_table.cpp b/src/core/backbone_router/ndproxy_table.cpp
index 89c0442..787a8a9 100644
--- a/src/core/backbone_router/ndproxy_table.cpp
+++ b/src/core/backbone_router/ndproxy_table.cpp
@@ -38,6 +38,7 @@
 #include "common/array.hpp"
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
+#include "common/num_utils.hpp"
 
 namespace ot {
 
@@ -66,10 +67,9 @@
 {
     OT_ASSERT(mValid);
 
-    mRloc16 = aRloc16;
-    aTimeSinceLastTransaction =
-        OT_MIN(aTimeSinceLastTransaction, static_cast<uint32_t>(Mle::kTimeSinceLastTransactionMax));
-    mLastRegistrationTime = TimerMilli::GetNow() - TimeMilli::SecToMsec(aTimeSinceLastTransaction);
+    mRloc16                   = aRloc16;
+    aTimeSinceLastTransaction = Min(aTimeSinceLastTransaction, Mle::kTimeSinceLastTransactionMax);
+    mLastRegistrationTime     = TimerMilli::GetNow() - TimeMilli::SecToMsec(aTimeSinceLastTransaction);
 }
 
 bool NdProxyTable::MatchesFilter(const NdProxy &aProxy, Filter aFilter)
@@ -123,10 +123,7 @@
     } while (mItem < GetArrayEnd(table.mProxies) && !MatchesFilter(*mItem, mFilter));
 }
 
-void NdProxyTable::Erase(NdProxy &aNdProxy)
-{
-    aNdProxy.mValid = false;
-}
+void NdProxyTable::Erase(NdProxy &aNdProxy) { aNdProxy.mValid = false; }
 
 void NdProxyTable::HandleDomainPrefixUpdate(Leader::DomainPrefixState aState)
 {
@@ -144,10 +141,7 @@
         proxy.Clear();
     }
 
-    if (mCallback != nullptr)
-    {
-        mCallback(mCallbackContext, OT_BACKBONE_ROUTER_NDPROXY_CLEARED, nullptr);
-    }
+    mCallback.InvokeIfSet(MapEnum(NdProxy::kCleared), nullptr);
 
     LogInfo("NdProxyTable::Clear!");
 }
@@ -155,7 +149,7 @@
 Error NdProxyTable::Register(const Ip6::InterfaceIdentifier &aAddressIid,
                              const Ip6::InterfaceIdentifier &aMeshLocalIid,
                              uint16_t                        aRloc16,
-                             const uint32_t *                aTimeSinceLastTransaction)
+                             const uint32_t                 *aTimeSinceLastTransaction)
 {
     Error    error                    = kErrorNone;
     NdProxy *proxy                    = FindByAddressIid(aAddressIid);
@@ -173,7 +167,7 @@
     proxy = FindByMeshLocalIid(aMeshLocalIid);
     if (proxy != nullptr)
     {
-        TriggerCallback(OT_BACKBONE_ROUTER_NDPROXY_REMOVED, proxy->mAddressIid);
+        TriggerCallback(NdProxy::kRemoved, proxy->mAddressIid);
         Erase(*proxy);
     }
     else
@@ -188,8 +182,8 @@
     mIsAnyDadInProcess = true;
 
 exit:
-    LogInfo("NdProxyTable::Register %s MLIID %s RLOC16 %04x LTT %u => %s", aAddressIid.ToString().AsCString(),
-            aMeshLocalIid.ToString().AsCString(), aRloc16, timeSinceLastTransaction, ErrorToString(error));
+    LogInfo("NdProxyTable::Register %s MLIID %s RLOC16 %04x LTT %lu => %s", aAddressIid.ToString().AsCString(),
+            aMeshLocalIid.ToString().AsCString(), aRloc16, ToUlong(timeSinceLastTransaction), ErrorToString(error));
     return error;
 }
 
@@ -271,26 +265,19 @@
     return;
 }
 
-void NdProxyTable::SetCallback(otBackboneRouterNdProxyCallback aCallback, void *aContext)
-{
-    mCallback        = aCallback;
-    mCallbackContext = aContext;
-}
-
-void NdProxyTable::TriggerCallback(otBackboneRouterNdProxyEvent    aEvent,
-                                   const Ip6::InterfaceIdentifier &aAddressIid) const
+void NdProxyTable::TriggerCallback(NdProxy::Event aEvent, const Ip6::InterfaceIdentifier &aAddressIid) const
 {
     Ip6::Address       dua;
     const Ip6::Prefix *prefix = Get<BackboneRouter::Leader>().GetDomainPrefix();
 
-    VerifyOrExit(mCallback != nullptr);
+    VerifyOrExit(mCallback.IsSet());
 
     OT_ASSERT(prefix != nullptr);
 
     dua.SetPrefix(*prefix);
     dua.SetIid(aAddressIid);
 
-    mCallback(mCallbackContext, aEvent, &dua);
+    mCallback.Invoke(MapEnum(aEvent), &dua);
 
 exit:
     return;
@@ -330,8 +317,7 @@
 {
     if (!aNdProxy.mDadFlag)
     {
-        TriggerCallback(aIsRenew ? OT_BACKBONE_ROUTER_NDPROXY_RENEWED : OT_BACKBONE_ROUTER_NDPROXY_ADDED,
-                        aNdProxy.mAddressIid);
+        TriggerCallback(aIsRenew ? NdProxy::kRenewed : NdProxy::kAdded, aNdProxy.mAddressIid);
 
         IgnoreError(Get<BackboneRouter::Manager>().SendProactiveBackboneNotification(
             GetDua(aNdProxy), aNdProxy.GetMeshLocalIid(), aNdProxy.GetTimeSinceLastTransaction()));
diff --git a/src/core/backbone_router/ndproxy_table.hpp b/src/core/backbone_router/ndproxy_table.hpp
index 097a6d3..8c12db1 100644
--- a/src/core/backbone_router/ndproxy_table.hpp
+++ b/src/core/backbone_router/ndproxy_table.hpp
@@ -40,6 +40,8 @@
 #include <openthread/backbone_router_ftd.h>
 
 #include "backbone_router/bbr_leader.hpp"
+#include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/iterator_utils.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
@@ -65,8 +67,23 @@
     class NdProxy : private Clearable<NdProxy>
     {
         friend class NdProxyTable;
+        friend class Clearable<NdProxy>;
 
     public:
+        typedef otBackboneRouterNdProxyCallback Callback; ///< ND Proxy callback.
+
+        /**
+         * This type represents the ND Proxy events.
+         *
+         */
+        enum Event
+        {
+            kAdded   = OT_BACKBONE_ROUTER_NDPROXY_ADDED,   ///< ND Proxy was added.
+            kRemoved = OT_BACKBONE_ROUTER_NDPROXY_REMOVED, ///< ND Proxy was removed.
+            kRenewed = OT_BACKBONE_ROUTER_NDPROXY_RENEWED, ///< ND Proxy was renewed.
+            kCleared = OT_BACKBONE_ROUTER_NDPROXY_CLEARED, ///< All ND Proxies were cleared.
+        };
+
         /**
          * This method gets the Mesh-Local IID of the ND Proxy.
          *
@@ -133,8 +150,6 @@
      */
     explicit NdProxyTable(Instance &aInstance)
         : InstanceLocator(aInstance)
-        , mCallback(nullptr)
-        , mCallbackContext(nullptr)
         , mIsAnyDadInProcess(false)
     {
     }
@@ -155,7 +170,7 @@
     Error Register(const Ip6::InterfaceIdentifier &aAddressIid,
                    const Ip6::InterfaceIdentifier &aMeshLocalIid,
                    uint16_t                        aRloc16,
-                   const uint32_t *                aTimeSinceLastTransaction);
+                   const uint32_t                 *aTimeSinceLastTransaction);
 
     /**
      * This method checks if a given IPv6 address IID was registered.
@@ -216,7 +231,7 @@
      * @param[in] aContext   A user context pointer.
      *
      */
-    void SetCallback(otBackboneRouterNdProxyCallback aCallback, void *aContext);
+    void SetCallback(NdProxy::Callback aCallback, void *aContext) { mCallback.Set(aCallback, aContext); }
 
     /**
      * This method retrieves the ND Proxy info of the Domain Unicast Address.
@@ -284,21 +299,22 @@
     IteratorBuilder Iterate(Filter aFilter) { return IteratorBuilder(GetInstance(), aFilter); }
     void            Clear(void);
     static bool     MatchesFilter(const NdProxy &aProxy, Filter aFilter);
-    NdProxy *       FindByAddressIid(const Ip6::InterfaceIdentifier &aAddressIid);
-    NdProxy *       FindByMeshLocalIid(const Ip6::InterfaceIdentifier &aMeshLocalIid);
-    NdProxy *       FindInvalid(void);
+    NdProxy        *FindByAddressIid(const Ip6::InterfaceIdentifier &aAddressIid);
+    NdProxy        *FindByMeshLocalIid(const Ip6::InterfaceIdentifier &aMeshLocalIid);
+    NdProxy        *FindInvalid(void);
     Ip6::Address    GetDua(NdProxy &aNdProxy);
     void            NotifyDuaRegistrationOnBackboneLink(NdProxy &aNdProxy, bool aIsRenew);
-    void TriggerCallback(otBackboneRouterNdProxyEvent aEvent, const Ip6::InterfaceIdentifier &aAddressIid) const;
+    void            TriggerCallback(NdProxy::Event aEvent, const Ip6::InterfaceIdentifier &aAddressIid) const;
 
-    NdProxy                         mProxies[kMaxNdProxyNum];
-    otBackboneRouterNdProxyCallback mCallback;
-    void *                          mCallbackContext;
-    bool                            mIsAnyDadInProcess : 1;
+    NdProxy                     mProxies[kMaxNdProxyNum];
+    Callback<NdProxy::Callback> mCallback;
+    bool                        mIsAnyDadInProcess : 1;
 };
 
 } // namespace BackboneRouter
 
+DefineMapEnum(otBackboneRouterNdProxyEvent, BackboneRouter::NdProxyTable::NdProxy::Event);
+
 } // namespace ot
 
 #endif // OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
diff --git a/src/core/border_router/infra_if.cpp b/src/core/border_router/infra_if.cpp
index 7fe20c9..acc3049 100644
--- a/src/core/border_router/infra_if.cpp
+++ b/src/core/border_router/infra_if.cpp
@@ -31,6 +31,7 @@
  */
 
 #include "infra_if.hpp"
+#include "common/num_utils.hpp"
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 
@@ -79,14 +80,14 @@
     LogInfo("Deinit");
 }
 
-bool InfraIf::HasAddress(const Ip6::Address &aAddress)
+bool InfraIf::HasAddress(const Ip6::Address &aAddress) const
 {
     OT_ASSERT(mInitialized);
 
     return otPlatInfraIfHasAddress(mIfIndex, &aAddress);
 }
 
-Error InfraIf::Send(const Icmp6Packet &aPacket, const Ip6::Address &aDestination)
+Error InfraIf::Send(const Icmp6Packet &aPacket, const Ip6::Address &aDestination) const
 {
     OT_ASSERT(mInitialized);
 
@@ -111,6 +112,33 @@
     }
 }
 
+Error InfraIf::DiscoverNat64Prefix(void) const
+{
+    OT_ASSERT(mInitialized);
+
+    return otPlatInfraIfDiscoverNat64Prefix(mIfIndex);
+}
+
+void InfraIf::DiscoverNat64PrefixDone(uint32_t aIfIndex, const Ip6::Prefix &aPrefix)
+{
+    Error error = kErrorNone;
+
+    OT_UNUSED_VARIABLE(aPrefix);
+
+    VerifyOrExit(mInitialized && mIsRunning, error = kErrorInvalidState);
+    VerifyOrExit(aIfIndex == mIfIndex, error = kErrorInvalidArgs);
+
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+    Get<RoutingManager>().HandleDiscoverNat64PrefixDone(aPrefix);
+#endif
+
+exit:
+    if (error != kErrorNone)
+    {
+        LogDebg("Failed to handle discovered NAT64 synthetic addresses: %s", ErrorToString(error));
+    }
+}
+
 Error InfraIf::HandleStateChanged(uint32_t aIfIndex, bool aIsRunning)
 {
     Error error = kErrorNone;
@@ -133,16 +161,16 @@
 {
     InfoString string;
 
-    string.Append("infra netif %u", mIfIndex);
+    string.Append("infra netif %lu", ToUlong(mIfIndex));
     return string;
 }
 
 //---------------------------------------------------------------------------------------------------------------------
 
-extern "C" void otPlatInfraIfRecvIcmp6Nd(otInstance *        aInstance,
+extern "C" void otPlatInfraIfRecvIcmp6Nd(otInstance         *aInstance,
                                          uint32_t            aInfraIfIndex,
                                          const otIp6Address *aSrcAddress,
-                                         const uint8_t *     aBuffer,
+                                         const uint8_t      *aBuffer,
                                          uint16_t            aBufferLength)
 {
     InfraIf::Icmp6Packet packet;
@@ -156,6 +184,13 @@
     return AsCoreType(aInstance).Get<InfraIf>().HandleStateChanged(aInfraIfIndex, aIsRunning);
 }
 
+extern "C" void otPlatInfraIfDiscoverNat64PrefixDone(otInstance        *aInstance,
+                                                     uint32_t           aInfraIfIndex,
+                                                     const otIp6Prefix *aIp6Prefix)
+{
+    AsCoreType(aInstance).Get<InfraIf>().DiscoverNat64PrefixDone(aInfraIfIndex, AsCoreType(aIp6Prefix));
+}
+
 } // namespace BorderRouter
 } // namespace ot
 
diff --git a/src/core/border_router/infra_if.hpp b/src/core/border_router/infra_if.hpp
index b67009e..ec4c548 100644
--- a/src/core/border_router/infra_if.hpp
+++ b/src/core/border_router/infra_if.hpp
@@ -125,7 +125,7 @@
      * @retval FALSE  The infrastructure interface does not have @p aAddress.
      *
      */
-    bool HasAddress(const Ip6::Address &aAddress);
+    bool HasAddress(const Ip6::Address &aAddress) const;
 
     /**
      * This method sends an ICMPv6 Neighbor Discovery packet on the infrastructure interface.
@@ -139,7 +139,7 @@
      * @retval kErrorFailed  Failed to send the ICMPv6 message.
      *
      */
-    Error Send(const Icmp6Packet &aPacket, const Ip6::Address &aDestination);
+    Error Send(const Icmp6Packet &aPacket, const Ip6::Address &aDestination) const;
 
     /**
      * This method processes a received ICMPv6 Neighbor Discovery packet from an infrastructure interface.
@@ -152,6 +152,26 @@
     void HandledReceived(uint32_t aIfIndex, const Ip6::Address &aSource, const Icmp6Packet &aPacket);
 
     /**
+     * This method sends a request to discover the NAT64 prefix on the infrastructure interface.
+     *
+     * @note  This method MUST be used when interface is initialized.
+     *
+     * @retval  kErrorNone    Successfully request NAT64 prefix discovery.
+     * @retval  kErrorFailed  Failed to request NAT64 prefix discovery.
+     *
+     */
+    Error DiscoverNat64Prefix(void) const;
+
+    /**
+     * This method processes the discovered NAT64 prefix.
+     *
+     * @param[in]  aIfIndex    The infrastructure interface index on which the host address is received.
+     * @param[in]  aPrefix     The NAT64 prefix on the infrastructure link.
+     *
+     */
+    void DiscoverNat64PrefixDone(uint32_t aIfIndex, const Ip6::Prefix &aPrefix);
+
+    /**
      * This method handles infrastructure interface state changes.
      *
      * @param[in]  aIfIndex         The infrastructure interface index.
diff --git a/src/core/border_router/routing_manager.cpp b/src/core/border_router/routing_manager.cpp
index c7f213f..24669f5 100644
--- a/src/core/border_router/routing_manager.cpp
+++ b/src/core/border_router/routing_manager.cpp
@@ -38,6 +38,7 @@
 
 #include <string.h>
 
+#include <openthread/border_router.h>
 #include <openthread/platform/infra_if.h>
 
 #include "common/code_utils.hpp"
@@ -45,10 +46,12 @@
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 #include "common/settings.hpp"
 #include "meshcop/extended_panid.hpp"
 #include "net/ip6.hpp"
+#include "net/nat64_translator.hpp"
 #include "thread/network_data_leader.hpp"
 #include "thread/network_data_local.hpp"
 #include "thread/network_data_notifier.hpp"
@@ -65,27 +68,19 @@
     , mIsEnabled(false)
     , mInfraIf(aInstance)
     , mLocalOmrPrefix(aInstance)
-    , mRouteInfoOptionPreference(NetworkData::kRoutePreferenceMedium)
-    , mIsAdvertisingLocalOnLinkPrefix(false)
-    , mOnLinkPrefixDeprecateTimer(aInstance, HandleOnLinkPrefixDeprecateTimer)
-    , mIsAdvertisingLocalNat64Prefix(false)
+    , mRioPreference(NetworkData::kRoutePreferenceLow)
+    , mUserSetRioPreference(false)
+    , mOnLinkPrefixManager(aInstance)
     , mDiscoveredPrefixTable(aInstance)
-    , mTimeRouterAdvMessageLastUpdate(TimerMilli::GetNow())
-    , mLearntRouterAdvMessageFromHost(false)
-    , mDiscoveredPrefixStaleTimer(aInstance, HandleDiscoveredPrefixStaleTimer)
-    , mRouterAdvertisementCount(0)
-    , mLastRouterAdvertisementSendTime(TimerMilli::GetNow() - kMinDelayBetweenRtrAdvs)
-    , mRouterSolicitTimer(aInstance, HandleRouterSolicitTimer)
-    , mRouterSolicitCount(0)
-    , mRoutingPolicyTimer(aInstance, HandleRoutingPolicyTimer)
+    , mRoutePublisher(aInstance)
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+    , mNat64PrefixManager(aInstance)
+#endif
+    , mRsSender(aInstance)
+    , mDiscoveredPrefixStaleTimer(aInstance)
+    , mRoutingPolicyTimer(aInstance)
 {
-    mFavoredDiscoveredOnLinkPrefix.Clear();
-
     mBrUlaPrefix.Clear();
-
-    mLocalOnLinkPrefix.Clear();
-
-    mLocalNat64Prefix.Clear();
 }
 
 Error RoutingManager::Init(uint32_t aInfraIfIndex, bool aInfraIfIsRunning)
@@ -96,10 +91,10 @@
 
     SuccessOrExit(error = LoadOrGenerateRandomBrUlaPrefix());
     mLocalOmrPrefix.GenerateFrom(mBrUlaPrefix);
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
-    GenerateNat64Prefix();
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+    mNat64PrefixManager.GenerateLocalPrefix(mBrUlaPrefix);
 #endif
-    GenerateOnLinkPrefix();
+    mOnLinkPrefixManager.Init();
 
     error = mInfraIf.HandleStateChanged(mInfraIf.GetIfIndex(), aInfraIfIsRunning);
 
@@ -127,20 +122,60 @@
     return error;
 }
 
+RoutingManager::State RoutingManager::GetState(void) const
+{
+    State state = kStateUninitialized;
+
+    VerifyOrExit(IsInitialized());
+    VerifyOrExit(IsEnabled(), state = kStateDisabled);
+
+    state = IsRunning() ? kStateRunning : kStateStopped;
+
+exit:
+    return state;
+}
+
 void RoutingManager::SetRouteInfoOptionPreference(RoutePreference aPreference)
 {
-    VerifyOrExit(mRouteInfoOptionPreference != aPreference);
+    LogInfo("User explicitly set RIO Preference to %s", RoutePreferenceToString(aPreference));
+    mUserSetRioPreference = true;
+    UpdateRioPreference(aPreference);
+}
 
-    mRouteInfoOptionPreference = aPreference;
+void RoutingManager::ClearRouteInfoOptionPreference(void)
+{
+    VerifyOrExit(mUserSetRioPreference);
 
-    VerifyOrExit(mIsRunning);
-    StartRoutingPolicyEvaluationJitter(kRoutingPolicyEvaluationJitter);
+    LogInfo("User cleared explicitly set RIO Preference");
+    mUserSetRioPreference = false;
+    SetRioPreferenceBasedOnRole();
 
 exit:
     return;
 }
 
-Error RoutingManager::GetOmrPrefix(Ip6::Prefix &aPrefix)
+void RoutingManager::SetRioPreferenceBasedOnRole(void)
+{
+    UpdateRioPreference(Get<Mle::Mle>().IsRouterOrLeader() ? NetworkData::kRoutePreferenceMedium
+                                                           : NetworkData::kRoutePreferenceLow);
+}
+
+void RoutingManager::UpdateRioPreference(RoutePreference aPreference)
+{
+    VerifyOrExit(mRioPreference != aPreference);
+
+    LogInfo("RIO Preference changed: %s -> %s", RoutePreferenceToString(mRioPreference),
+            RoutePreferenceToString(aPreference));
+    mRioPreference = aPreference;
+
+    VerifyOrExit(mIsRunning);
+    ScheduleRoutingPolicyEvaluation(kAfterRandomDelay);
+
+exit:
+    return;
+}
+
+Error RoutingManager::GetOmrPrefix(Ip6::Prefix &aPrefix) const
 {
     Error error = kErrorNone;
 
@@ -151,11 +186,11 @@
     return error;
 }
 
-Error RoutingManager::GetFavoredOmrPrefix(Ip6::Prefix &aPrefix, RoutePreference &aPreference)
+Error RoutingManager::GetFavoredOmrPrefix(Ip6::Prefix &aPrefix, RoutePreference &aPreference) const
 {
     Error error = kErrorNone;
 
-    VerifyOrExit(IsInitialized(), error = kErrorInvalidState);
+    VerifyOrExit(IsRunning(), error = kErrorInvalidState);
     aPrefix     = mFavoredOmrPrefix.GetPrefix();
     aPreference = mFavoredOmrPrefix.GetPreference();
 
@@ -163,24 +198,57 @@
     return error;
 }
 
-Error RoutingManager::GetOnLinkPrefix(Ip6::Prefix &aPrefix)
+Error RoutingManager::GetOnLinkPrefix(Ip6::Prefix &aPrefix) const
 {
     Error error = kErrorNone;
 
     VerifyOrExit(IsInitialized(), error = kErrorInvalidState);
-    aPrefix = mLocalOnLinkPrefix;
+    aPrefix = mOnLinkPrefixManager.GetLocalPrefix();
 
 exit:
     return error;
 }
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
+Error RoutingManager::GetFavoredOnLinkPrefix(Ip6::Prefix &aPrefix) const
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(IsInitialized(), error = kErrorInvalidState);
+    aPrefix = mOnLinkPrefixManager.GetFavoredDiscoveredPrefix();
+
+    if (aPrefix.GetLength() == 0)
+    {
+        aPrefix = mOnLinkPrefixManager.GetLocalPrefix();
+    }
+
+exit:
+    return error;
+}
+
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+void RoutingManager::SetNat64PrefixManagerEnabled(bool aEnabled)
+{
+    // PrefixManager will start itself if routing manager is running.
+    mNat64PrefixManager.SetEnabled(aEnabled);
+}
+
 Error RoutingManager::GetNat64Prefix(Ip6::Prefix &aPrefix)
 {
     Error error = kErrorNone;
 
     VerifyOrExit(IsInitialized(), error = kErrorInvalidState);
-    aPrefix = mLocalNat64Prefix;
+    aPrefix = mNat64PrefixManager.GetLocalPrefix();
+
+exit:
+    return error;
+}
+
+Error RoutingManager::GetFavoredNat64Prefix(Ip6::Prefix &aPrefix, RoutePreference &aRoutePreference)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(IsInitialized(), error = kErrorInvalidState);
+    aPrefix = mNat64PrefixManager.GetFavoredPrefix(aRoutePreference);
 
 exit:
     return error;
@@ -220,32 +288,6 @@
     return error;
 }
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
-void RoutingManager::GenerateNat64Prefix(void)
-{
-    mLocalNat64Prefix = mBrUlaPrefix;
-    mLocalNat64Prefix.SetSubnetId(kNat64PrefixSubnetId);
-    mLocalNat64Prefix.mPrefix.mFields.m32[2] = 0;
-    mLocalNat64Prefix.SetLength(kNat64PrefixLength);
-
-    LogInfo("Generated NAT64 prefix: %s", mLocalNat64Prefix.ToString().AsCString());
-}
-#endif
-
-void RoutingManager::GenerateOnLinkPrefix(void)
-{
-    MeshCoP::ExtendedPanId extPanId = Get<MeshCoP::ExtendedPanIdManager>().GetExtPanId();
-
-    mLocalOnLinkPrefix.mPrefix.mFields.m8[0] = 0xfd;
-    // Global ID: 40 most significant bits of Extended PAN ID
-    memcpy(mLocalOnLinkPrefix.mPrefix.mFields.m8 + 1, extPanId.m8, 5);
-    // Subnet ID: 16 least significant bits of Extended PAN ID
-    memcpy(mLocalOnLinkPrefix.mPrefix.mFields.m8 + 6, extPanId.m8 + 6, 2);
-    mLocalOnLinkPrefix.SetLength(kOnLinkPrefixLength);
-
-    LogNote("Local on-link prefix: %s", mLocalOnLinkPrefix.ToString().AsCString());
-}
-
 void RoutingManager::EvaluateState(void)
 {
     if (mIsEnabled && Get<Mle::MleRouter>().IsAttached() && mInfraIf.IsRunning())
@@ -266,7 +308,13 @@
 
         mIsRunning = true;
         UpdateDiscoveredPrefixTableOnNetDataChange();
-        StartRouterSolicitationDelay();
+        mOnLinkPrefixManager.Start();
+        DetermineFavoredOmrPrefix();
+        mRoutePublisher.Start();
+        mRsSender.Start();
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+        mNat64PrefixManager.Start();
+#endif
     }
 }
 
@@ -277,47 +325,61 @@
     mLocalOmrPrefix.RemoveFromNetData();
     mFavoredOmrPrefix.Clear();
 
-    mFavoredDiscoveredOnLinkPrefix.Clear();
+    mOnLinkPrefixManager.Stop();
 
-    if (mIsAdvertisingLocalOnLinkPrefix)
-    {
-        UnpublishExternalRoute(mLocalOnLinkPrefix);
-
-        // Start deprecating the local on-link prefix to send a PIO
-        // with zero preferred lifetime in `SendRouterAdvertisement`.
-        DeprecateOnLinkPrefix();
-    }
-
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
-    if (mIsAdvertisingLocalNat64Prefix)
-    {
-        UnpublishExternalRoute(mLocalNat64Prefix);
-        mIsAdvertisingLocalNat64Prefix = false;
-    }
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+    mNat64PrefixManager.Stop();
 #endif
+
     SendRouterAdvertisement(kInvalidateAllPrevPrefixes);
 
     mAdvertisedPrefixes.Clear();
-    mOnLinkPrefixDeprecateTimer.Stop();
 
     mDiscoveredPrefixTable.RemoveAllEntries();
     mDiscoveredPrefixStaleTimer.Stop();
 
-    mRouterAdvertisementCount = 0;
+    mRaInfo.mTxCount = 0;
 
-    mRouterSolicitTimer.Stop();
-    mRouterSolicitCount = 0;
+    mRsSender.Stop();
 
     mRoutingPolicyTimer.Stop();
 
+    mRoutePublisher.Stop();
+
     LogInfo("Border Routing manager stopped");
 
     mIsRunning = false;
 
+#if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
+    if (Get<Srp::Server>().IsAutoEnableMode())
+    {
+        Get<Srp::Server>().Disable();
+    }
+#endif
+
 exit:
     return;
 }
 
+#if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
+void RoutingManager::HandleSrpServerAutoEnableMode(void)
+{
+    VerifyOrExit(Get<Srp::Server>().IsAutoEnableMode());
+
+    if (IsInitalPolicyEvaluationDone())
+    {
+        Get<Srp::Server>().Enable();
+    }
+    else
+    {
+        Get<Srp::Server>().Disable();
+    }
+
+exit:
+    return;
+}
+#endif
+
 void RoutingManager::HandleReceived(const InfraIf::Icmp6Packet &aPacket, const Ip6::Address &aSrcAddress)
 {
     const Ip6::Icmp::Header *icmp6Header;
@@ -334,6 +396,9 @@
     case Ip6::Icmp::Header::kTypeRouterSolicit:
         HandleRouterSolicit(aPacket, aSrcAddress);
         break;
+    case Ip6::Icmp::Header::kTypeNeighborAdvert:
+        HandleNeighborAdvertisement(aPacket);
+        break;
     default:
         break;
     }
@@ -344,6 +409,12 @@
 
 void RoutingManager::HandleNotifierEvents(Events aEvents)
 {
+    if (aEvents.Contains(kEventThreadRoleChanged) && !mUserSetRioPreference)
+    {
+        SetRioPreferenceBasedOnRole();
+        mRoutePublisher.HandleRoleChanged();
+    }
+
     VerifyOrExit(IsInitialized() && IsEnabled());
 
     if (aEvents.Contains(kEventThreadRoleChanged))
@@ -354,25 +425,13 @@
     if (mIsRunning && aEvents.Contains(kEventThreadNetdataChanged))
     {
         UpdateDiscoveredPrefixTableOnNetDataChange();
-        StartRoutingPolicyEvaluationJitter(kRoutingPolicyEvaluationJitter);
+        mOnLinkPrefixManager.HandleNetDataChange();
+        ScheduleRoutingPolicyEvaluation(kAfterRandomDelay);
     }
 
     if (aEvents.Contains(kEventThreadExtPanIdChanged))
     {
-        if (mIsAdvertisingLocalOnLinkPrefix)
-        {
-            UnpublishExternalRoute(mLocalOnLinkPrefix);
-            // TODO: consider deprecating/invalidating existing
-            // on-link prefix
-            mIsAdvertisingLocalOnLinkPrefix = false;
-        }
-
-        GenerateOnLinkPrefix();
-
-        if (mIsRunning)
-        {
-            StartRoutingPolicyEvaluationJitter(kRoutingPolicyEvaluationJitter);
-        }
+        mOnLinkPrefixManager.HandleExtPanIdChange();
     }
 
 exit:
@@ -383,7 +442,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
@@ -396,35 +454,17 @@
             continue;
         }
 
-        mDiscoveredPrefixTable.RemoveRoutePrefix(prefixConfig.GetPrefix(),
-                                                 DiscoveredPrefixTable::kUnpublishFromNetData);
-
-        if (prefixConfig.mDefaultRoute)
-        {
-            foundDefRouteOmrPrefix = true;
-        }
+        mDiscoveredPrefixTable.RemoveRoutePrefix(prefixConfig.GetPrefix());
     }
-
-    // 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)
+void RoutingManager::DetermineFavoredOmrPrefix(void)
 {
+    // Determine the favored OMR prefix present in Network Data.
+
     NetworkData::Iterator           iterator = NetworkData::kIteratorInit;
     NetworkData::OnMeshPrefixConfig onMeshPrefixConfig;
 
-    OT_ASSERT(mIsRunning);
-
     mFavoredOmrPrefix.Clear();
 
     while (Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, onMeshPrefixConfig) == kErrorNone)
@@ -439,6 +479,13 @@
             mFavoredOmrPrefix.SetFrom(onMeshPrefixConfig);
         }
     }
+}
+
+void RoutingManager::EvaluateOmrPrefix(void)
+{
+    OT_ASSERT(mIsRunning);
+
+    DetermineFavoredOmrPrefix();
 
     // Decide if we need to add or remove our local OMR prefix.
 
@@ -446,7 +493,7 @@
     {
         LogInfo("EvaluateOmrPrefix: No preferred OMR prefix found in Thread network");
 
-        // The `aNewPrefixes` remains empty if we fail to publish
+        // The `mFavoredOmrPrefix` remains empty if we fail to publish
         // the local OMR prefix.
         SuccessOrExit(mLocalOmrPrefix.AddToNetData());
 
@@ -468,171 +515,6 @@
     return;
 }
 
-Error RoutingManager::PublishExternalRoute(const Ip6::Prefix &aPrefix, RoutePreference aRoutePreference, bool aNat64)
-{
-    Error                            error;
-    NetworkData::ExternalRouteConfig routeConfig;
-
-    OT_ASSERT(mIsRunning);
-
-    routeConfig.Clear();
-    routeConfig.SetPrefix(aPrefix);
-    routeConfig.mStable     = true;
-    routeConfig.mNat64      = aNat64;
-    routeConfig.mPreference = aRoutePreference;
-
-    error = Get<NetworkData::Publisher>().PublishExternalRoute(routeConfig);
-
-    if (error != kErrorNone)
-    {
-        LogWarn("Failed to publish external route %s: %s", aPrefix.ToString().AsCString(), ErrorToString(error));
-    }
-
-    return error;
-}
-
-void RoutingManager::UnpublishExternalRoute(const Ip6::Prefix &aPrefix)
-{
-    VerifyOrExit(mIsRunning);
-    IgnoreError(Get<NetworkData::Publisher>().UnpublishPrefix(aPrefix));
-
-exit:
-    return;
-}
-
-void RoutingManager::EvaluateOnLinkPrefix(void)
-{
-    VerifyOrExit(!IsRouterSolicitationInProgress());
-
-    mDiscoveredPrefixTable.FindFavoredOnLinkPrefix(mFavoredDiscoveredOnLinkPrefix);
-
-    if (mFavoredDiscoveredOnLinkPrefix.GetLength() == 0)
-    {
-        // We need to advertise our local on-link prefix since there is
-        // no discovered on-link prefix.
-
-        mOnLinkPrefixDeprecateTimer.Stop();
-        VerifyOrExit(!mIsAdvertisingLocalOnLinkPrefix);
-
-        SuccessOrExit(PublishExternalRoute(mLocalOnLinkPrefix, NetworkData::kRoutePreferenceMedium));
-
-        mIsAdvertisingLocalOnLinkPrefix = true;
-        LogInfo("Start advertising on-link prefix %s on %s", mLocalOnLinkPrefix.ToString().AsCString(),
-                mInfraIf.ToString().AsCString());
-
-        // We remove the local on-link prefix from discovered prefix
-        // table, in case it was previously discovered and included in
-        // the table (now as a deprecating entry). We remove it with
-        // `kKeepInNetData` flag to ensure that the prefix is not
-        // unpublished from network data.
-        //
-        // Note that `ShouldProcessPrefixInfoOption()` will also check
-        // not allow the local on-link prefix to be added in the prefix
-        // table while we are advertising it.
-
-        mDiscoveredPrefixTable.RemoveOnLinkPrefix(mLocalOnLinkPrefix, DiscoveredPrefixTable::kKeepInNetData);
-    }
-    else
-    {
-        VerifyOrExit(mIsAdvertisingLocalOnLinkPrefix);
-
-        // When an application-specific on-link prefix is received and
-        // it is larger than the local prefix, we will not remove the
-        // advertised local prefix. In this case, there will be two
-        // on-link prefixes on the infra link. But all BRs will still
-        // converge to the same smallest/favored on-link prefix and the
-        // application-specific prefix is not used.
-
-        if (!(mLocalOnLinkPrefix < mFavoredDiscoveredOnLinkPrefix))
-        {
-            LogInfo("EvaluateOnLinkPrefix: There is already favored on-link prefix %s on %s",
-                    mFavoredDiscoveredOnLinkPrefix.ToString().AsCString(), mInfraIf.ToString().AsCString());
-            DeprecateOnLinkPrefix();
-        }
-    }
-
-exit:
-    return;
-}
-
-void RoutingManager::HandleOnLinkPrefixDeprecateTimer(Timer &aTimer)
-{
-    aTimer.Get<RoutingManager>().HandleOnLinkPrefixDeprecateTimer();
-}
-
-void RoutingManager::HandleOnLinkPrefixDeprecateTimer(void)
-{
-    OT_ASSERT(!mIsAdvertisingLocalOnLinkPrefix);
-
-    LogInfo("Local on-link prefix %s expired", mLocalOnLinkPrefix.ToString().AsCString());
-
-    if (!mDiscoveredPrefixTable.ContainsOnLinkPrefix(mLocalOnLinkPrefix))
-    {
-        UnpublishExternalRoute(mLocalOnLinkPrefix);
-    }
-}
-
-void RoutingManager::DeprecateOnLinkPrefix(void)
-{
-    OT_ASSERT(mIsAdvertisingLocalOnLinkPrefix);
-
-    mIsAdvertisingLocalOnLinkPrefix = false;
-
-    LogInfo("Deprecate local on-link prefix %s", mLocalOnLinkPrefix.ToString().AsCString());
-    mOnLinkPrefixDeprecateTimer.StartAt(mTimeAdvertisedOnLinkPrefix,
-                                        TimeMilli::SecToMsec(kDefaultOnLinkPrefixLifetime));
-}
-
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
-void RoutingManager::EvaluateNat64Prefix(void)
-{
-    OT_ASSERT(mIsRunning);
-
-    NetworkData::Iterator            iterator = NetworkData::kIteratorInit;
-    NetworkData::ExternalRouteConfig config;
-    Ip6::Prefix                      smallestNat64Prefix;
-
-    LogInfo("Evaluating NAT64 prefix");
-
-    smallestNat64Prefix.Clear();
-    while (Get<NetworkData::Leader>().GetNextExternalRoute(iterator, config) == kErrorNone)
-    {
-        const Ip6::Prefix &prefix = config.GetPrefix();
-
-        if (config.mNat64 && prefix.IsValidNat64())
-        {
-            if (smallestNat64Prefix.GetLength() == 0 || prefix < smallestNat64Prefix)
-            {
-                smallestNat64Prefix = prefix;
-            }
-        }
-    }
-
-    if (smallestNat64Prefix.GetLength() == 0 || smallestNat64Prefix == mLocalNat64Prefix)
-    {
-        LogInfo("No NAT64 prefix in Network Data is smaller than the local NAT64 prefix %s",
-                mLocalNat64Prefix.ToString().AsCString());
-
-        // Advertise local NAT64 prefix.
-        if (!mIsAdvertisingLocalNat64Prefix &&
-            PublishExternalRoute(mLocalNat64Prefix, NetworkData::kRoutePreferenceLow, /* aNat64= */ true) == kErrorNone)
-        {
-            mIsAdvertisingLocalNat64Prefix = true;
-        }
-    }
-    else if (mIsAdvertisingLocalNat64Prefix && smallestNat64Prefix < mLocalNat64Prefix)
-    {
-        // Withdraw local NAT64 prefix if it's not the smallest one in Network Data.
-        // TODO: remove the prefix with lower preference after discovering upstream NAT64 prefix is supported
-        LogNote("Withdrawing local NAT64 prefix since a smaller one %s exists.",
-                smallestNat64Prefix.ToString().AsCString());
-
-        UnpublishExternalRoute(mLocalNat64Prefix);
-        mIsAdvertisingLocalNat64Prefix = false;
-    }
-}
-#endif
-
 // This method evaluate the routing policy depends on prefix and route
 // information on Thread Network and infra link. As a result, this
 // method May send RA messages on infra link and publish/unpublish
@@ -643,134 +525,105 @@
 
     LogInfo("Evaluating routing policy");
 
-    // 0. Evaluate on-link, OMR and NAT64 prefixes.
-    EvaluateOnLinkPrefix();
+    mOnLinkPrefixManager.Evaluate();
     EvaluateOmrPrefix();
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
-    EvaluateNat64Prefix();
+    mRoutePublisher.Evaluate();
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+    mNat64PrefixManager.Evaluate();
 #endif
 
-    // 1. Send Router Advertisement message if necessary.
     SendRouterAdvertisement(kAdvPrefixesFromNetData);
 
-    // 2. Schedule routing policy timer with random interval for the next Router Advertisement.
+#if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
+    if (Get<Srp::Server>().IsAutoEnableMode() && IsInitalPolicyEvaluationDone())
     {
-        uint32_t nextSendDelay;
+        // If SRP server uses the auto-enable mode, we enable the SRP
+        // server on the first RA transmission after we are done with
+        // initial prefix/route configurations. Note that if SRP server
+        // is already enabled, calling `Enable()` again does nothing.
 
-        nextSendDelay = Random::NonCrypto::GetUint32InRange(kMinRtrAdvInterval, kMaxRtrAdvInterval);
-
-        if (mRouterAdvertisementCount <= kMaxInitRtrAdvertisements && nextSendDelay > kMaxInitRtrAdvInterval)
-        {
-            nextSendDelay = kMaxInitRtrAdvInterval;
-        }
-
-        StartRoutingPolicyEvaluationDelay(Time::SecToMsec(nextSendDelay));
+        Get<Srp::Server>().Enable();
     }
+#endif
+
+    ScheduleRoutingPolicyEvaluation(kForNextRa);
 }
 
-void RoutingManager::StartRoutingPolicyEvaluationJitter(uint32_t aJitterMilli)
+bool RoutingManager::IsInitalPolicyEvaluationDone(void) const
 {
-    OT_ASSERT(mIsRunning);
+    // This method indicates whether or not we are done with the
+    // initial policy evaluation and prefix and route setup, i.e.,
+    // the OMR and on-link prefixes are determined, advertised in
+    // the emitted Router Advert message on infrastructure side
+    // and published in the Thread Network Data.
 
-    StartRoutingPolicyEvaluationDelay(Random::NonCrypto::GetUint32InRange(0, aJitterMilli));
+    return mIsRunning && !mFavoredOmrPrefix.IsEmpty() && mOnLinkPrefixManager.IsInitalEvaluationDone();
 }
 
-void RoutingManager::StartRoutingPolicyEvaluationDelay(uint32_t aDelayMilli)
+void RoutingManager::ScheduleRoutingPolicyEvaluation(ScheduleMode aMode)
 {
-    TimeMilli now          = TimerMilli::GetNow();
-    TimeMilli evaluateTime = now + aDelayMilli;
-    TimeMilli earliestTime = mLastRouterAdvertisementSendTime + kMinDelayBetweenRtrAdvs;
+    TimeMilli now   = TimerMilli::GetNow();
+    uint32_t  delay = 0;
+    TimeMilli evaluateTime;
 
-    evaluateTime = OT_MAX(evaluateTime, earliestTime);
+    switch (aMode)
+    {
+    case kImmediately:
+        break;
 
-    LogInfo("Start evaluating routing policy, scheduled in %u milliseconds", evaluateTime - now);
+    case kForNextRa:
+        delay = Random::NonCrypto::GetUint32InRange(Time::SecToMsec(kMinRtrAdvInterval),
+                                                    Time::SecToMsec(kMaxRtrAdvInterval));
+
+        if (mRaInfo.mTxCount <= kMaxInitRtrAdvertisements && delay > Time::SecToMsec(kMaxInitRtrAdvInterval))
+        {
+            delay = Time::SecToMsec(kMaxInitRtrAdvInterval);
+        }
+        break;
+
+    case kAfterRandomDelay:
+        delay = Random::NonCrypto::GetUint32InRange(kPolicyEvaluationMinDelay, kPolicyEvaluationMaxDelay);
+        break;
+
+    case kToReplyToRs:
+        delay = Random::NonCrypto::GetUint32InRange(0, kRaReplyJitter);
+        break;
+    }
+
+    // Ensure we wait a min delay after last RA tx
+    evaluateTime = Max(now + delay, mRaInfo.mLastTxTime + kMinDelayBetweenRtrAdvs);
+
+    LogInfo("Start evaluating routing policy, scheduled in %lu milliseconds", ToUlong(evaluateTime - now));
 
     mRoutingPolicyTimer.FireAtIfEarlier(evaluateTime);
 }
 
-// starts sending Router Solicitations in random delay
-// between 0 and kMaxRtrSolicitationDelay.
-void RoutingManager::StartRouterSolicitationDelay(void)
-{
-    uint32_t randomDelay;
-
-    VerifyOrExit(!IsRouterSolicitationInProgress());
-
-    OT_ASSERT(mRouterSolicitCount == 0);
-
-    static_assert(kMaxRtrSolicitationDelay > 0, "invalid maximum Router Solicitation delay");
-    randomDelay = Random::NonCrypto::GetUint32InRange(0, Time::SecToMsec(kMaxRtrSolicitationDelay));
-
-    LogInfo("Start Router Solicitation, scheduled in %u milliseconds", randomDelay);
-    mTimeRouterSolicitStart = TimerMilli::GetNow();
-    mRouterSolicitTimer.Start(randomDelay);
-
-exit:
-    return;
-}
-
-bool RoutingManager::IsRouterSolicitationInProgress(void) const
-{
-    return mRouterSolicitTimer.IsRunning() || mRouterSolicitCount > 0;
-}
-
-Error RoutingManager::SendRouterSolicitation(void)
-{
-    Ip6::Address                  destAddress;
-    Ip6::Nd::RouterSolicitMessage routerSolicit;
-    InfraIf::Icmp6Packet          packet;
-
-    OT_ASSERT(IsInitialized());
-
-    packet.InitFrom(routerSolicit);
-    destAddress.SetToLinkLocalAllRoutersMulticast();
-
-    return mInfraIf.Send(packet, destAddress);
-}
-
 void RoutingManager::SendRouterAdvertisement(RouterAdvTxMode aRaTxMode)
 {
     // RA message max length is derived to accommodate:
     //
-    // - The RA header,
-    // - At most one PIO (for local on-link prefix),
+    // - The RA header.
+    // - One PIO for current local on-link prefix.
+    // - At most `kMaxOldPrefixes` for old deprecating on-link prefixes.
     // - At most twice `kMaxOnMeshPrefixes` RIO for on-mesh prefixes.
     //   Factor two is used for RIO to account for entries invalidating
     //   previous prefixes while adding new ones.
 
     static constexpr uint16_t kMaxRaLength =
         sizeof(Ip6::Nd::RouterAdvertMessage::Header) + sizeof(Ip6::Nd::PrefixInfoOption) +
+        sizeof(Ip6::Nd::PrefixInfoOption) * OnLinkPrefixManager::kMaxOldPrefixes +
         2 * kMaxOnMeshPrefixes * (sizeof(Ip6::Nd::RouteInfoOption) + sizeof(Ip6::Prefix));
 
     uint8_t                         buffer[kMaxRaLength];
-    Ip6::Nd::RouterAdvertMessage    raMsg(mRouterAdvertHeader, buffer);
+    Ip6::Nd::RouterAdvertMessage    raMsg(mRaInfo.mHeader, buffer);
     NetworkData::Iterator           iterator;
     NetworkData::OnMeshPrefixConfig prefixConfig;
 
-    // Append PIO for local on-link prefix. Ensure it is either being
-    // advertised or deprecated.
+    // Append PIO for local on-link prefix if is either being
+    // advertised or deprecated and for old prefix if is being
+    // deprecated.
 
-    if (mIsAdvertisingLocalOnLinkPrefix || mOnLinkPrefixDeprecateTimer.IsRunning())
-    {
-        uint32_t validLifetime     = kDefaultOnLinkPrefixLifetime;
-        uint32_t preferredLifetime = kDefaultOnLinkPrefixLifetime;
-
-        if (mOnLinkPrefixDeprecateTimer.IsRunning())
-        {
-            validLifetime     = TimeMilli::MsecToSec(mOnLinkPrefixDeprecateTimer.GetFireTime() - TimerMilli::GetNow());
-            preferredLifetime = 0;
-        }
-
-        SuccessOrAssert(raMsg.AppendPrefixInfoOption(mLocalOnLinkPrefix, validLifetime, preferredLifetime));
-
-        if (mIsAdvertisingLocalOnLinkPrefix)
-        {
-            mTimeAdvertisedOnLinkPrefix = TimerMilli::GetNow();
-        }
-
-        LogInfo("RouterAdvert: Added PIO for %s (valid=%u, preferred=%u)", mLocalOnLinkPrefix.ToString().AsCString(),
-                validLifetime, preferredLifetime);
-    }
+    mOnLinkPrefixManager.AppendAsPiosTo(raMsg);
 
     // Determine which previously advertised prefixes need to be
     // invalidated. Under `kInvalidateAllPrevPrefixes` mode we need
@@ -806,7 +659,7 @@
     {
         if (prefix.GetLength() != 0)
         {
-            SuccessOrAssert(raMsg.AppendRouteInfoOption(prefix, /* aRouteLifetime */ 0, mRouteInfoOptionPreference));
+            SuccessOrAssert(raMsg.AppendRouteInfoOption(prefix, /* aRouteLifetime */ 0, mRioPreference));
             LogInfo("RouterAdvert: Added RIO for %s (lifetime=0)", prefix.ToString().AsCString());
         }
     }
@@ -833,7 +686,7 @@
 
         // (2) Favored OMR prefix.
 
-        if (!mFavoredOmrPrefix.IsEmpty())
+        if (!mFavoredOmrPrefix.IsEmpty() && !mFavoredOmrPrefix.IsDomainPrefix())
         {
             mAdvertisedPrefixes.Add(mFavoredOmrPrefix.GetPrefix());
         }
@@ -855,6 +708,11 @@
             // leader and can take some time to be updated in Network
             // Data.
 
+            if (prefixConfig.mDp)
+            {
+                continue;
+            }
+
             if (IsValidOmrPrefix(prefixConfig) && (prefixConfig.GetPrefix() != mLocalOmrPrefix.GetPrefix()))
             {
                 mAdvertisedPrefixes.Add(prefixConfig.GetPrefix());
@@ -875,9 +733,9 @@
 
         for (const OnMeshPrefix &prefix : mAdvertisedPrefixes)
         {
-            SuccessOrAssert(raMsg.AppendRouteInfoOption(prefix, kDefaultOmrPrefixLifetime, mRouteInfoOptionPreference));
-            LogInfo("RouterAdvert: Added RIO for %s (lifetime=%u)", prefix.ToString().AsCString(),
-                    kDefaultOmrPrefixLifetime);
+            SuccessOrAssert(raMsg.AppendRouteInfoOption(prefix, kDefaultOmrPrefixLifetime, mRioPreference));
+            LogInfo("RouterAdvert: Added RIO for %s (lifetime=%lu)", prefix.ToString().AsCString(),
+                    ToUlong(kDefaultOmrPrefixLifetime));
         }
     }
 
@@ -886,7 +744,7 @@
         Error        error;
         Ip6::Address destAddress;
 
-        ++mRouterAdvertisementCount;
+        ++mRaInfo.mTxCount;
 
         destAddress.SetToLinkLocalAllNodesMulticast();
 
@@ -894,13 +752,15 @@
 
         if (error == kErrorNone)
         {
-            mLastRouterAdvertisementSendTime = TimerMilli::GetNow();
+            mRaInfo.mLastTxTime = TimerMilli::GetNow();
+            Get<Ip6::Ip6>().GetBorderRoutingCounters().mRaTxSuccess++;
             LogInfo("Sent Router Advertisement on %s", mInfraIf.ToString().AsCString());
             DumpDebg("[BR-CERT] direction=send | type=RA |", raMsg.GetAsPacket().GetBytes(),
                      raMsg.GetAsPacket().GetLength());
         }
         else
         {
+            Get<Ip6::Ip6>().GetBorderRoutingCounters().mRaTxFailure++;
             LogWarn("Failed to send Router Advertisement on %s: %s", mInfraIf.ToString().AsCString(),
                     ErrorToString(error));
         }
@@ -924,14 +784,19 @@
         {
         case Ip6::Nd::Option::kTypePrefixInfo:
         {
-            // PIO should match `mLocalOnLinkPrefix`.
-
             const Ip6::Nd::PrefixInfoOption &pio = static_cast<const Ip6::Nd::PrefixInfoOption &>(option);
 
             VerifyOrExit(pio.IsValid());
             pio.GetPrefix(prefix);
 
-            VerifyOrExit(prefix == mLocalOnLinkPrefix);
+            // If it is a non-deprecated PIO, it should match the
+            // local on-link prefix.
+
+            if (pio.GetPreferredLifetime() > 0)
+            {
+                VerifyOrExit(prefix == mOnLinkPrefixManager.GetLocalPrefix());
+            }
+
             break;
         }
 
@@ -977,14 +842,13 @@
 bool RoutingManager::IsValidOmrPrefix(const NetworkData::OnMeshPrefixConfig &aOnMeshPrefixConfig)
 {
     return IsValidOmrPrefix(aOnMeshPrefixConfig.GetPrefix()) && aOnMeshPrefixConfig.mOnMesh &&
-           aOnMeshPrefixConfig.mSlaac && aOnMeshPrefixConfig.mStable && !aOnMeshPrefixConfig.mDp;
+           aOnMeshPrefixConfig.mSlaac && aOnMeshPrefixConfig.mStable;
 }
 
-bool RoutingManager::IsValidOmrPrefix(const Ip6::Prefix &aOmrPrefix)
+bool RoutingManager::IsValidOmrPrefix(const Ip6::Prefix &aPrefix)
 {
-    // Accept ULA prefix with length of 64 bits and GUA prefix.
-    return (aOmrPrefix.mLength == kOmrPrefixLength && aOmrPrefix.mPrefix.mFields.m8[0] == 0xfd) ||
-           (aOmrPrefix.mLength >= 3 && (aOmrPrefix.GetBytes()[0] & 0xE0) == 0x20);
+    // Accept ULA/GUA prefixes with 64-bit length.
+    return (aPrefix.GetLength() == kOmrPrefixLength) && !aPrefix.IsLinkLocal() && !aPrefix.IsMulticast();
 }
 
 bool RoutingManager::IsValidOnLinkPrefix(const Ip6::Nd::PrefixInfoOption &aPio)
@@ -1002,77 +866,31 @@
            !aOnLinkPrefix.IsMulticast();
 }
 
-void RoutingManager::HandleRouterSolicitTimer(Timer &aTimer)
+void RoutingManager::HandleRsSenderFinished(TimeMilli aStartTime)
 {
-    aTimer.Get<RoutingManager>().HandleRouterSolicitTimer();
-}
+    // This is a callback from `RsSender` and is invoked when it
+    // finishes a cycle of sending Router Solicitations. `aStartTime`
+    // specifies the start time of the RS transmission cycle.
+    //
+    // We remove or deprecate old entries in discovered table that are
+    // not refreshed during Router Solicitation. We also invalidate
+    // the learned RA header if it is not refreshed during Router
+    // Solicitation.
 
-void RoutingManager::HandleRouterSolicitTimer(void)
-{
-    LogInfo("Router solicitation times out");
+    mDiscoveredPrefixTable.RemoveOrDeprecateOldEntries(aStartTime);
 
-    if (mRouterSolicitCount < kMaxRtrSolicitations)
+    if (mRaInfo.mHeaderUpdateTime <= aStartTime)
     {
-        uint32_t nextSolicitationDelay;
-        Error    error;
-
-        error = SendRouterSolicitation();
-
-        if (error == kErrorNone)
-        {
-            LogDebg("Successfully sent %uth Router Solicitation", mRouterSolicitCount);
-            ++mRouterSolicitCount;
-            nextSolicitationDelay =
-                (mRouterSolicitCount == kMaxRtrSolicitations) ? kMaxRtrSolicitationDelay : kRtrSolicitationInterval;
-        }
-        else
-        {
-            LogCrit("Failed to send %uth Router Solicitation: %s", mRouterSolicitCount, ErrorToString(error));
-
-            // It's unexpected that RS will fail and we will retry sending RS messages in 60 seconds.
-            // Notice that `mRouterSolicitCount` is not incremented for failed RS and thus we will
-            // not start configuring on-link prefixes before `kMaxRtrSolicitations` successful RS
-            // messages have been sent.
-            nextSolicitationDelay = kRtrSolicitationRetryDelay;
-            mRouterSolicitCount   = 0;
-        }
-
-        LogDebg("Router solicitation timer scheduled in %u seconds", nextSolicitationDelay);
-        mRouterSolicitTimer.Start(Time::SecToMsec(nextSolicitationDelay));
+        UpdateRouterAdvertHeader(/* aRouterAdvertMessage */ nullptr);
     }
-    else
-    {
-        // Remove route prefixes and deprecate on-link prefixes that
-        // are not refreshed during Router Solicitation.
-        mDiscoveredPrefixTable.RemoveOrDeprecateOldEntries(mTimeRouterSolicitStart);
 
-        // Invalidate the learned RA message if it is not refreshed during Router Solicitation.
-        if (mTimeRouterAdvMessageLastUpdate <= mTimeRouterSolicitStart)
-        {
-            UpdateRouterAdvertHeader(/* aRouterAdvertMessage */ nullptr);
-        }
-
-        mRouterSolicitCount = 0;
-
-        // Re-evaluate our routing policy and send Router Advertisement if necessary.
-        StartRoutingPolicyEvaluationDelay(/* aDelayJitter */ 0);
-    }
-}
-
-void RoutingManager::HandleDiscoveredPrefixStaleTimer(Timer &aTimer)
-{
-    aTimer.Get<RoutingManager>().HandleDiscoveredPrefixStaleTimer();
+    ScheduleRoutingPolicyEvaluation(kImmediately);
 }
 
 void RoutingManager::HandleDiscoveredPrefixStaleTimer(void)
 {
     LogInfo("Stale On-Link or OMR Prefixes or RA messages are detected");
-    StartRouterSolicitationDelay();
-}
-
-void RoutingManager::HandleRoutingPolicyTimer(Timer &aTimer)
-{
-    aTimer.Get<RoutingManager>().EvaluateRoutingPolicy();
+    mRsSender.Start();
 }
 
 void RoutingManager::HandleRouterSolicit(const InfraIf::Icmp6Packet &aPacket, const Ip6::Address &aSrcAddress)
@@ -1080,11 +898,24 @@
     OT_UNUSED_VARIABLE(aPacket);
     OT_UNUSED_VARIABLE(aSrcAddress);
 
+    Get<Ip6::Ip6>().GetBorderRoutingCounters().mRsRx++;
     LogInfo("Received Router Solicitation from %s on %s", aSrcAddress.ToString().AsCString(),
             mInfraIf.ToString().AsCString());
 
-    // Schedule routing policy evaluation with random jitter to respond with Router Advertisement.
-    StartRoutingPolicyEvaluationJitter(kRaReplyJitter);
+    ScheduleRoutingPolicyEvaluation(kToReplyToRs);
+}
+
+void RoutingManager::HandleNeighborAdvertisement(const InfraIf::Icmp6Packet &aPacket)
+{
+    const Ip6::Nd::NeighborAdvertMessage *naMsg;
+
+    VerifyOrExit(aPacket.GetLength() >= sizeof(naMsg));
+    naMsg = reinterpret_cast<const Ip6::Nd::NeighborAdvertMessage *>(aPacket.GetBytes());
+
+    mDiscoveredPrefixTable.ProcessNeighborAdvertMessage(*naMsg);
+
+exit:
+    return;
 }
 
 void RoutingManager::HandleRouterAdvertisement(const InfraIf::Icmp6Packet &aPacket, const Ip6::Address &aSrcAddress)
@@ -1095,6 +926,7 @@
 
     VerifyOrExit(routerAdvMessage.IsValid());
 
+    Get<Ip6::Ip6>().GetBorderRoutingCounters().mRaRx++;
     LogInfo("Received Router Advertisement from %s on %s", aSrcAddress.ToString().AsCString(),
             mInfraIf.ToString().AsCString());
     DumpDebg("[BR-CERT] direction=recv | type=RA |", aPacket.GetBytes(), aPacket.GetLength());
@@ -1127,9 +959,9 @@
         ExitNow();
     }
 
-    if (mIsAdvertisingLocalOnLinkPrefix)
+    if (mOnLinkPrefixManager.IsPublishingOrAdvertising())
     {
-        VerifyOrExit(aPrefix != mLocalOnLinkPrefix);
+        VerifyOrExit(aPrefix != mOnLinkPrefixManager.GetLocalPrefix());
     }
 
     shouldProcess = true;
@@ -1185,22 +1017,13 @@
 void RoutingManager::HandleDiscoveredPrefixTableChanged(void)
 {
     // This is a callback from `mDiscoveredPrefixTable` indicating that
-    // there has been a change in the table. If the favored on-link
-    // prefix has changed, we trigger a re-evaluation of the routing
-    // policy.
-
-    Ip6::Prefix newFavoredPrefix;
+    // there has been a change in the table.
 
     VerifyOrExit(mIsRunning);
 
     ResetDiscoveredPrefixStaleTimer();
-
-    mDiscoveredPrefixTable.FindFavoredOnLinkPrefix(newFavoredPrefix);
-
-    if (newFavoredPrefix != mFavoredDiscoveredOnLinkPrefix)
-    {
-        StartRoutingPolicyEvaluationJitter(kRoutingPolicyEvaluationJitter);
-    }
+    mOnLinkPrefixManager.HandleDiscoveredPrefixTableChanged();
+    mRoutePublisher.Evaluate();
 
 exit:
     return;
@@ -1210,23 +1033,45 @@
 {
     NetworkData::Iterator           iterator = NetworkData::kIteratorInit;
     NetworkData::OnMeshPrefixConfig onMeshPrefixConfig;
-    bool                            contain = false;
+    bool                            contains = false;
 
     while (Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, onMeshPrefixConfig) == kErrorNone)
     {
         if (IsValidOmrPrefix(onMeshPrefixConfig) && onMeshPrefixConfig.GetPrefix() == aPrefix)
         {
-            contain = true;
+            contains = true;
             break;
         }
     }
 
-    return contain;
+    return contains;
+}
+
+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 && RoutePublisher::GetUlaPrefix().ContainsPrefix(routeConfig.GetPrefix()))
+        {
+            contains = true;
+            break;
+        }
+    }
+
+    return contains;
 }
 
 void RoutingManager::UpdateRouterAdvertHeader(const Ip6::Nd::RouterAdvertMessage *aRouterAdvertMessage)
 {
-    // Updates the `mRouterAdvertHeader` from the given RA message.
+    // Updates the `mRaInfo` from the given RA message.
 
     Ip6::Nd::RouterAdvertMessage::Header oldHeader;
 
@@ -1238,34 +1083,34 @@
         VerifyOrExit(!IsReceivedRouterAdvertFromManager(*aRouterAdvertMessage));
     }
 
-    oldHeader                       = mRouterAdvertHeader;
-    mTimeRouterAdvMessageLastUpdate = TimerMilli::GetNow();
+    oldHeader                 = mRaInfo.mHeader;
+    mRaInfo.mHeaderUpdateTime = TimerMilli::GetNow();
 
     if (aRouterAdvertMessage == nullptr || aRouterAdvertMessage->GetHeader().GetRouterLifetime() == 0)
     {
-        mRouterAdvertHeader.SetToDefault();
-        mLearntRouterAdvMessageFromHost = false;
+        mRaInfo.mHeader.SetToDefault();
+        mRaInfo.mIsHeaderFromHost = false;
     }
     else
     {
-        // The checksum is set to zero in `mRouterAdvertHeader`
+        // The checksum is set to zero in `mRaInfo.mHeader`
         // which indicates to platform that it needs to do the
         // calculation and update it.
 
-        mRouterAdvertHeader = aRouterAdvertMessage->GetHeader();
-        mRouterAdvertHeader.SetChecksum(0);
-        mLearntRouterAdvMessageFromHost = true;
+        mRaInfo.mHeader = aRouterAdvertMessage->GetHeader();
+        mRaInfo.mHeader.SetChecksum(0);
+        mRaInfo.mIsHeaderFromHost = true;
     }
 
     ResetDiscoveredPrefixStaleTimer();
 
-    if (mRouterAdvertHeader != oldHeader)
+    if (mRaInfo.mHeader != oldHeader)
     {
         // If there was a change to the header, start timer to
         // reevaluate routing policy and send RA message with new
         // header.
 
-        StartRoutingPolicyEvaluationJitter(kRoutingPolicyEvaluationJitter);
+        ScheduleRoutingPolicyEvaluation(kAfterRandomDelay);
     }
 
 exit:
@@ -1285,11 +1130,11 @@
     nextStaleTime = mDiscoveredPrefixTable.CalculateNextStaleTime(now);
 
     // Check for stale Router Advertisement Message if learnt from Host.
-    if (mLearntRouterAdvMessageFromHost)
+    if (mRaInfo.mIsHeaderFromHost)
     {
-        TimeMilli raStaleTime = OT_MAX(now, mTimeRouterAdvMessageLastUpdate + Time::SecToMsec(kRtrAdvStaleTime));
+        TimeMilli raStaleTime = Max(now, mRaInfo.mHeaderUpdateTime + Time::SecToMsec(kRtrAdvStaleTime));
 
-        nextStaleTime = OT_MIN(nextStaleTime, raStaleTime);
+        nextStaleTime = Min(nextStaleTime, raStaleTime);
     }
 
     if (nextStaleTime == now.GetDistantFuture())
@@ -1304,7 +1149,7 @@
     else
     {
         mDiscoveredPrefixStaleTimer.FireAt(nextStaleTime);
-        LogDebg("Prefix stale timer scheduled in %lu ms", nextStaleTime - now);
+        LogDebg("Prefix stale timer scheduled in %lu ms", ToUlong(nextStaleTime - now));
     }
 }
 
@@ -1313,14 +1158,14 @@
 
 RoutingManager::DiscoveredPrefixTable::DiscoveredPrefixTable(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mTimer(aInstance, HandleTimer)
-    , mSignalTask(aInstance, HandleSignalTask)
-    , mAllowDefaultRouteInNetData(false)
+    , mEntryTimer(aInstance)
+    , mRouterTimer(aInstance)
+    , mSignalTask(aInstance)
 {
 }
 
 void RoutingManager::DiscoveredPrefixTable::ProcessRouterAdvertMessage(const Ip6::Nd::RouterAdvertMessage &aRaMessage,
-                                                                       const Ip6::Address &                aSrcAddress)
+                                                                       const Ip6::Address                 &aSrcAddress)
 {
     // Process a received RA message and update the prefix table.
 
@@ -1365,6 +1210,8 @@
         }
     }
 
+    UpdateRouterOnRx(*router);
+
     RemoveRoutersWithNoEntries();
 
 exit:
@@ -1372,9 +1219,9 @@
 }
 
 void RoutingManager::DiscoveredPrefixTable::ProcessDefaultRoute(const Ip6::Nd::RouterAdvertMessage::Header &aRaHeader,
-                                                                Router &                                    aRouter)
+                                                                Router                                     &aRouter)
 {
-    Entry *     entry;
+    Entry      *entry;
     Ip6::Prefix prefix;
 
     prefix.Clear();
@@ -1400,8 +1247,8 @@
         entry->SetFrom(aRaHeader);
     }
 
-    UpdateNetworkDataOnChangeTo(*entry);
-    mTimer.FireAtIfEarlier(entry->GetExpireTime());
+    mEntryTimer.FireAtIfEarlier(entry->GetExpireTime());
+
     SignalTableChanged();
 
 exit:
@@ -1409,17 +1256,17 @@
 }
 
 void RoutingManager::DiscoveredPrefixTable::ProcessPrefixInfoOption(const Ip6::Nd::PrefixInfoOption &aPio,
-                                                                    Router &                         aRouter)
+                                                                    Router                          &aRouter)
 {
     Ip6::Prefix prefix;
-    Entry *     entry;
+    Entry      *entry;
 
     VerifyOrExit(aPio.IsValid());
     aPio.GetPrefix(prefix);
 
     VerifyOrExit(Get<RoutingManager>().ShouldProcessPrefixInfoOption(aPio, prefix));
 
-    LogInfo("Processing PIO (%s, %u seconds)", prefix.ToString().AsCString(), aPio.GetValidLifetime());
+    LogInfo("Processing PIO (%s, %lu seconds)", prefix.ToString().AsCString(), ToUlong(aPio.GetValidLifetime()));
 
     entry = aRouter.mEntries.FindMatching(Entry::Matcher(prefix, Entry::kTypeOnLink));
 
@@ -1443,11 +1290,11 @@
         Entry newEntry;
 
         newEntry.SetFrom(aPio);
-        entry->AdoptValidAndPreferredLiftimesFrom(newEntry);
+        entry->AdoptValidAndPreferredLifetimesFrom(newEntry);
     }
 
-    UpdateNetworkDataOnChangeTo(*entry);
-    mTimer.FireAtIfEarlier(entry->GetExpireTime());
+    mEntryTimer.FireAtIfEarlier(entry->GetExpireTime());
+
     SignalTableChanged();
 
 exit:
@@ -1455,17 +1302,17 @@
 }
 
 void RoutingManager::DiscoveredPrefixTable::ProcessRouteInfoOption(const Ip6::Nd::RouteInfoOption &aRio,
-                                                                   Router &                        aRouter)
+                                                                   Router                         &aRouter)
 {
     Ip6::Prefix prefix;
-    Entry *     entry;
+    Entry      *entry;
 
     VerifyOrExit(aRio.IsValid());
     aRio.GetPrefix(prefix);
 
     VerifyOrExit(Get<RoutingManager>().ShouldProcessRouteInfoOption(aRio, prefix));
 
-    LogInfo("Processing RIO (%s, %u seconds)", prefix.ToString().AsCString(), aRio.GetRouteLifetime());
+    LogInfo("Processing RIO (%s, %lu seconds)", prefix.ToString().AsCString(), ToUlong(aRio.GetRouteLifetime()));
 
     entry = aRouter.mEntries.FindMatching(Entry::Matcher(prefix, Entry::kTypeRoute));
 
@@ -1489,40 +1336,43 @@
         entry->SetFrom(aRio);
     }
 
-    UpdateNetworkDataOnChangeTo(*entry);
-    mTimer.FireAtIfEarlier(entry->GetExpireTime());
+    mEntryTimer.FireAtIfEarlier(entry->GetExpireTime());
+
     SignalTableChanged();
 
 exit:
     return;
 }
 
-void RoutingManager::DiscoveredPrefixTable::SetAllowDefaultRouteInNetData(bool aAllow)
+bool RoutingManager::DiscoveredPrefixTable::Contains(const Entry::Checker &aChecker) const
 {
-    Entry *     favoredEntry;
-    Ip6::Prefix prefix;
+    bool contains = false;
 
-    VerifyOrExit(aAllow != mAllowDefaultRouteInNetData);
-
-    LogInfo("Allow default route in netdata: %s -> %s", ToYesNo(mAllowDefaultRouteInNetData), ToYesNo(aAllow));
-
-    mAllowDefaultRouteInNetData = aAllow;
-
-    prefix.Clear();
-    favoredEntry = FindFavoredEntryToPublish(prefix);
-    VerifyOrExit(favoredEntry != nullptr);
-
-    if (mAllowDefaultRouteInNetData)
+    for (const Router &router : mRouters)
     {
-        PublishEntry(*favoredEntry);
-    }
-    else
-    {
-        UnpublishEntry(*favoredEntry);
+        if (router.mEntries.ContainsMatching(aChecker))
+        {
+            contains = true;
+            break;
+        }
     }
 
-exit:
-    return;
+    return contains;
+}
+
+bool RoutingManager::DiscoveredPrefixTable::ContainsDefaultOrNonUlaRoutePrefix(void) const
+{
+    return Contains(Entry::Checker(Entry::Checker::kIsNotUla, Entry::kTypeRoute));
+}
+
+bool RoutingManager::DiscoveredPrefixTable::ContainsNonUlaOnLinkPrefix(void) const
+{
+    return Contains(Entry::Checker(Entry::Checker::kIsNotUla, Entry::kTypeOnLink));
+}
+
+bool RoutingManager::DiscoveredPrefixTable::ContainsUlaOnLinkPrefix(void) const
+{
+    return Contains(Entry::Checker(Entry::Checker::kIsUla, Entry::kTypeOnLink));
 }
 
 void RoutingManager::DiscoveredPrefixTable::FindFavoredOnLinkPrefix(Ip6::Prefix &aPrefix) const
@@ -1550,48 +1400,19 @@
     }
 }
 
-bool RoutingManager::DiscoveredPrefixTable::ContainsOnLinkPrefix(const Ip6::Prefix &aPrefix) const
+void RoutingManager::DiscoveredPrefixTable::RemoveOnLinkPrefix(const Ip6::Prefix &aPrefix)
 {
-    return ContainsPrefix(Entry::Matcher(aPrefix, Entry::kTypeOnLink));
+    RemovePrefix(Entry::Matcher(aPrefix, Entry::kTypeOnLink));
 }
 
-bool RoutingManager::DiscoveredPrefixTable::ContainsRoutePrefix(const Ip6::Prefix &aPrefix) const
+void RoutingManager::DiscoveredPrefixTable::RemoveRoutePrefix(const Ip6::Prefix &aPrefix)
 {
-    return ContainsPrefix(Entry::Matcher(aPrefix, Entry::kTypeRoute));
+    RemovePrefix(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, NetDataMode aNetDataMode)
-{
-    RemovePrefix(Entry::Matcher(aPrefix, Entry::kTypeOnLink), aNetDataMode);
-}
-
-void RoutingManager::DiscoveredPrefixTable::RemoveRoutePrefix(const Ip6::Prefix &aPrefix, NetDataMode aNetDataMode)
-{
-    RemovePrefix(Entry::Matcher(aPrefix, Entry::kTypeRoute), aNetDataMode);
-}
-
-void RoutingManager::DiscoveredPrefixTable::RemovePrefix(const Entry::Matcher &aMatcher, NetDataMode aNetDataMode)
+void RoutingManager::DiscoveredPrefixTable::RemovePrefix(const Entry::Matcher &aMatcher)
 {
     // Removes all entries matching a given prefix from the table.
-    // `aNetDataMode` specifies behavior when a match is found and
-    // removed. It indicates whether or not to unpublish it from
-    // Network Data.
 
     LinkedList<Entry> removedEntries;
 
@@ -1602,11 +1423,6 @@
 
     VerifyOrExit(!removedEntries.IsEmpty());
 
-    if (aNetDataMode == kUnpublishFromNetData)
-    {
-        UnpublishEntry(*removedEntries.GetHead());
-    }
-
     FreeEntries(removedEntries);
     RemoveRoutersWithNoEntries();
 
@@ -1627,14 +1443,13 @@
 
         while ((entry = router.mEntries.Pop()) != nullptr)
         {
-            UnpublishEntry(*entry);
             FreeEntry(*entry);
             SignalTableChanged();
         }
     }
 
     RemoveRoutersWithNoEntries();
-    mTimer.Stop();
+    mEntryTimer.Stop();
 }
 
 void RoutingManager::DiscoveredPrefixTable::RemoveOrDeprecateOldEntries(TimeMilli aTimeThreshold)
@@ -1665,6 +1480,36 @@
     RemoveExpiredEntries();
 }
 
+void RoutingManager::DiscoveredPrefixTable::RemoveOrDeprecateEntriesFromInactiveRouters(void)
+{
+    // Remove route prefix entries and deprecate on-link prefix entries
+    // in the table for routers that have reached the max NS probe
+    // attempts and considered as inactive.
+
+    for (Router &router : mRouters)
+    {
+        if (router.mNsProbeCount <= Router::kMaxNsProbes)
+        {
+            continue;
+        }
+
+        for (Entry &entry : router.mEntries)
+        {
+            if (entry.IsOnLinkPrefix() && !entry.IsDeprecated())
+            {
+                entry.ClearPreferredLifetime();
+                SignalTableChanged();
+            }
+            else
+            {
+                entry.ClearValidLifetime();
+            }
+        }
+    }
+
+    RemoveExpiredEntries();
+}
+
 TimeMilli RoutingManager::DiscoveredPrefixTable::CalculateNextStaleTime(TimeMilli aNow) const
 {
     TimeMilli onLinkStaleTime = aNow;
@@ -1679,22 +1524,22 @@
     {
         for (const Entry &entry : router.mEntries)
         {
-            TimeMilli entryStaleTime = OT_MAX(aNow, entry.GetStaleTime());
+            TimeMilli entryStaleTime = Max(aNow, entry.GetStaleTime());
 
             if (entry.IsOnLinkPrefix() && !entry.IsDeprecated())
             {
-                onLinkStaleTime = OT_MAX(onLinkStaleTime, entryStaleTime);
+                onLinkStaleTime = Max(onLinkStaleTime, entryStaleTime);
                 foundOnLink     = true;
             }
 
             if (!entry.IsOnLinkPrefix())
             {
-                routeStaleTime = OT_MIN(routeStaleTime, entryStaleTime);
+                routeStaleTime = Min(routeStaleTime, entryStaleTime);
             }
         }
     }
 
-    return foundOnLink ? OT_MIN(onLinkStaleTime, routeStaleTime) : routeStaleTime;
+    return foundOnLink ? Min(onLinkStaleTime, routeStaleTime) : routeStaleTime;
 }
 
 void RoutingManager::DiscoveredPrefixTable::RemoveRoutersWithNoEntries(void)
@@ -1715,8 +1560,8 @@
     }
 }
 
-RoutingManager::DiscoveredPrefixTable::Entry *RoutingManager::DiscoveredPrefixTable::FindFavoredEntryToPublish(
-    const Ip6::Prefix &aPrefix)
+const RoutingManager::DiscoveredPrefixTable::Entry *RoutingManager::DiscoveredPrefixTable::FindFavoredEntryToPublish(
+    const Ip6::Prefix &aPrefix) const
 {
     // Finds the favored entry matching a given `aPrefix` in the table
     // to publish in the Network Data. We can have multiple entries
@@ -1725,11 +1570,11 @@
     // select the one with the highest preference as the favored
     // entry to publish.
 
-    Entry *favoredEntry = nullptr;
+    const Entry *favoredEntry = nullptr;
 
-    for (Router &router : mRouters)
+    for (const Router &router : mRouters)
     {
-        for (Entry &entry : router.mEntries)
+        for (const Entry &entry : router.mEntries)
         {
             if (entry.GetPrefix() != aPrefix)
             {
@@ -1746,50 +1591,7 @@
     return favoredEntry;
 }
 
-void RoutingManager::DiscoveredPrefixTable::UpdateNetworkDataOnChangeTo(Entry &aEntry)
-{
-    // Updates Network Data when there is a change to `aEntry` which
-    // can be a newly added entry or an existing entry that is
-    // modified due to processing of a received RA message.
-
-    Entry *favoredEntry;
-
-    if (aEntry.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(aEntry.GetPrefix());
-
-    OT_ASSERT(favoredEntry != nullptr);
-    PublishEntry(*favoredEntry);
-
-exit:
-    return;
-}
-
-void RoutingManager::DiscoveredPrefixTable::PublishEntry(const Entry &aEntry)
-{
-    IgnoreError(Get<RoutingManager>().PublishExternalRoute(aEntry.GetPrefix(), aEntry.GetPreference()));
-}
-
-void RoutingManager::DiscoveredPrefixTable::UnpublishEntry(const Entry &aEntry)
-{
-    Get<RoutingManager>().UnpublishExternalRoute(aEntry.GetPrefix());
-}
-
-void RoutingManager::DiscoveredPrefixTable::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<RoutingManager>().mDiscoveredPrefixTable.HandleTimer();
-}
-
-void RoutingManager::DiscoveredPrefixTable::HandleTimer(void)
-{
-    RemoveExpiredEntries();
-}
+void RoutingManager::DiscoveredPrefixTable::HandleEntryTimer(void) { RemoveExpiredEntries(); }
 
 void RoutingManager::DiscoveredPrefixTable::RemoveExpiredEntries(void)
 {
@@ -1804,23 +1606,6 @@
 
     RemoveRoutersWithNoEntries();
 
-    // Determine if we need to publish/unpublish any prefixes in
-    // the Network Data.
-
-    for (const Entry &expiredEntry : expiredEntries)
-    {
-        Entry *favoredEntry = FindFavoredEntryToPublish(expiredEntry.GetPrefix());
-
-        if (favoredEntry == nullptr)
-        {
-            UnpublishEntry(expiredEntry);
-        }
-        else
-        {
-            PublishEntry(*favoredEntry);
-        }
-    }
-
     if (!expiredEntries.IsEmpty())
     {
         SignalTableChanged();
@@ -1834,24 +1619,113 @@
     {
         for (const Entry &entry : router.mEntries)
         {
-            nextExpireTime = OT_MIN(nextExpireTime, entry.GetExpireTime());
+            nextExpireTime = Min(nextExpireTime, entry.GetExpireTime());
         }
     }
 
     if (nextExpireTime != now.GetDistantFuture())
     {
-        mTimer.FireAt(nextExpireTime);
+        mEntryTimer.FireAt(nextExpireTime);
     }
 }
 
-void RoutingManager::DiscoveredPrefixTable::SignalTableChanged(void)
+void RoutingManager::DiscoveredPrefixTable::SignalTableChanged(void) { mSignalTask.Post(); }
+
+void RoutingManager::DiscoveredPrefixTable::ProcessNeighborAdvertMessage(
+    const Ip6::Nd::NeighborAdvertMessage &aNaMessage)
 {
-    mSignalTask.Post();
+    Router *router;
+
+    VerifyOrExit(aNaMessage.IsValid());
+
+    router = mRouters.FindMatching(aNaMessage.GetTargetAddress());
+    VerifyOrExit(router != nullptr);
+
+    LogInfo("Received NA from router %s", router->mAddress.ToString().AsCString());
+
+    UpdateRouterOnRx(*router);
+
+exit:
+    return;
 }
 
-void RoutingManager::DiscoveredPrefixTable::HandleSignalTask(Tasklet &aTasklet)
+void RoutingManager::DiscoveredPrefixTable::UpdateRouterOnRx(Router &aRouter)
 {
-    aTasklet.Get<RoutingManager>().HandleDiscoveredPrefixTableChanged();
+    aRouter.mNsProbeCount = 0;
+    aRouter.mTimeout = TimerMilli::GetNow() + Random::NonCrypto::AddJitter(Router::kActiveTimeout, Router::kJitter);
+
+    mRouterTimer.FireAtIfEarlier(aRouter.mTimeout);
+}
+
+void RoutingManager::DiscoveredPrefixTable::HandleRouterTimer(void)
+{
+    TimeMilli now      = TimerMilli::GetNow();
+    TimeMilli nextTime = now.GetDistantFuture();
+
+    for (Router &router : mRouters)
+    {
+        if (router.mNsProbeCount > Router::kMaxNsProbes)
+        {
+            continue;
+        }
+
+        // If the `router` emitting RA has an address belonging to
+        // infra interface, it indicates that the RAs are from
+        // same device. In this case we skip performing NS probes.
+        // This addresses situation where platform may not be
+        // be able to receive and pass the NA message response
+        // from device itself.
+
+        if (Get<RoutingManager>().mInfraIf.HasAddress(router.mAddress))
+        {
+            continue;
+        }
+
+        if (router.mTimeout <= now)
+        {
+            router.mNsProbeCount++;
+
+            if (router.mNsProbeCount > Router::kMaxNsProbes)
+            {
+                LogInfo("No response to all Neighbor Solicitations attempts from router %s",
+                        router.mAddress.ToString().AsCString());
+                continue;
+            }
+
+            router.mTimeout = now + ((router.mNsProbeCount < Router::kMaxNsProbes) ? Router::kNsProbeRetryInterval
+                                                                                   : Router::kNsProbeTimeout);
+
+            SendNeighborSolicitToRouter(router);
+        }
+
+        nextTime = Min(nextTime, router.mTimeout);
+    }
+
+    RemoveOrDeprecateEntriesFromInactiveRouters();
+
+    if (nextTime != now.GetDistantFuture())
+    {
+        mRouterTimer.FireAtIfEarlier(nextTime);
+    }
+}
+
+void RoutingManager::DiscoveredPrefixTable::SendNeighborSolicitToRouter(const Router &aRouter)
+{
+    InfraIf::Icmp6Packet            packet;
+    Ip6::Nd::NeighborSolicitMessage neighborSolicitMsg;
+
+    VerifyOrExit(!Get<RoutingManager>().mRsSender.IsInProgress());
+
+    neighborSolicitMsg.SetTargetAddress(aRouter.mAddress);
+    packet.InitFrom(neighborSolicitMsg);
+
+    IgnoreError(Get<RoutingManager>().mInfraIf.Send(packet, aRouter.mAddress));
+
+    LogInfo("Sent Neighbor Solicitation to %s - attempt:%u/%u", aRouter.mAddress.ToString().AsCString(),
+            aRouter.mNsProbeCount, Router::kMaxNsProbes);
+
+exit:
+    return;
 }
 
 void RoutingManager::DiscoveredPrefixTable::InitIterator(PrefixTableIterator &aIterator) const
@@ -1864,7 +1738,7 @@
 }
 
 Error RoutingManager::DiscoveredPrefixTable::GetNextEntry(PrefixTableIterator &aIterator,
-                                                          PrefixTableEntry &   aEntry) const
+                                                          PrefixTableEntry    &aEntry) const
 {
     Error     error    = kErrorNone;
     Iterator &iterator = static_cast<Iterator &>(aIterator);
@@ -1941,9 +1815,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
@@ -1953,7 +1832,7 @@
 
 TimeMilli RoutingManager::DiscoveredPrefixTable::Entry::GetStaleTime(void) const
 {
-    uint32_t delay = OT_MIN(kRtrAdvStaleTime, IsOnLinkPrefix() ? GetPreferredLifetime() : mValidLifetime);
+    uint32_t delay = Min(kRtrAdvStaleTime, IsOnLinkPrefix() ? GetPreferredLifetime() : mValidLifetime);
 
     return mLastUpdateTime + TimeMilli::SecToMsec(delay);
 }
@@ -1973,7 +1852,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;
 
@@ -2020,16 +1899,27 @@
 //---------------------------------------------------------------------------------------------------------------------
 // OmrPrefix
 
+bool RoutingManager::OmrPrefix::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::OmrPrefix::SetFrom(const NetworkData::OnMeshPrefixConfig &aOnMeshPrefixConfig)
 {
-    mPrefix     = aOnMeshPrefixConfig.GetPrefix();
-    mPreference = aOnMeshPrefixConfig.GetPreference();
+    mPrefix         = aOnMeshPrefixConfig.GetPrefix();
+    mPreference     = aOnMeshPrefixConfig.GetPreference();
+    mIsDomainPrefix = aOnMeshPrefixConfig.mDp;
 }
 
 void RoutingManager::OmrPrefix::SetFrom(const LocalOmrPrefix &aLocalOmrPrefix)
 {
-    mPrefix     = aLocalOmrPrefix.GetPrefix();
-    mPreference = aLocalOmrPrefix.GetPreference();
+    mPrefix         = aLocalOmrPrefix.GetPrefix();
+    mPreference     = aLocalOmrPrefix.GetPreference();
+    mIsDomainPrefix = false;
 }
 
 bool RoutingManager::OmrPrefix::IsFavoredOver(const NetworkData::OnMeshPrefixConfig &aOmrPrefixConfig) const
@@ -2057,6 +1947,7 @@
 RoutingManager::LocalOmrPrefix::LocalOmrPrefix(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mIsAddedInNetData(false)
+    , mDefaultRoute(false)
 {
 }
 
@@ -2071,10 +1962,23 @@
 
 Error RoutingManager::LocalOmrPrefix::AddToNetData(void)
 {
-    Error                           error = kErrorNone;
-    NetworkData::OnMeshPrefixConfig config;
+    Error error = kErrorNone;
 
     VerifyOrExit(!mIsAddedInNetData);
+    SuccessOrExit(error = AddOrUpdate());
+    mIsAddedInNetData = true;
+
+exit:
+    return error;
+}
+
+Error RoutingManager::LocalOmrPrefix::AddOrUpdate(void)
+{
+    // Add the local OMR prefix in Thread Network Data or update it
+    // (e.g., change default route flag) if it is already added.
+
+    Error                           error;
+    NetworkData::OnMeshPrefixConfig config;
 
     config.Clear();
     config.mPrefix       = mPrefix;
@@ -2082,21 +1986,21 @@
     config.mSlaac        = true;
     config.mPreferred    = true;
     config.mOnMesh       = true;
-    config.mDefaultRoute = false;
+    config.mDefaultRoute = mDefaultRoute;
     config.mPreference   = GetPreference();
 
     error = Get<NetworkData::Local>().AddOnMeshPrefix(config);
 
     if (error != kErrorNone)
     {
-        LogWarn("Failed to add local OMR prefix %s in Thread Network Data: %s", mPrefix.ToString().AsCString(),
-                ErrorToString(error));
+        LogWarn("Failed to %s %s in Thread Network Data: %s", !mIsAddedInNetData ? "add" : "update",
+                ToString().AsCString(), ErrorToString(error));
         ExitNow();
     }
 
-    mIsAddedInNetData = true;
     Get<NetworkData::Notifier>().HandleServerDataUpdated();
-    LogInfo("Added local OMR prefix %s in Thread Network Data", mPrefix.ToString().AsCString());
+
+    LogInfo("%s %s in Thread Network Data", !mIsAddedInNetData ? "Added" : "Updated", ToString().AsCString());
 
 exit:
     return error;
@@ -2112,19 +2016,564 @@
 
     if (error != kErrorNone)
     {
-        LogWarn("Failed to remove local OMR prefix %s from Thread Network Data: %s", mPrefix.ToString().AsCString(),
-                ErrorToString(error));
+        LogWarn("Failed to remove %s from Thread Network Data: %s", ToString().AsCString(), ErrorToString(error));
         ExitNow();
     }
 
     mIsAddedInNetData = false;
     Get<NetworkData::Notifier>().HandleServerDataUpdated();
-    LogInfo("Removed local OMR prefix %s from Thread Network Data", mPrefix.ToString().AsCString());
+    LogInfo("Removed %s from Thread Network Data", ToString().AsCString());
 
 exit:
     return;
 }
 
+void RoutingManager::LocalOmrPrefix::UpdateDefaultRouteFlag(bool aDefaultRoute)
+{
+    VerifyOrExit(aDefaultRoute != mDefaultRoute);
+
+    mDefaultRoute = aDefaultRoute;
+
+    VerifyOrExit(mIsAddedInNetData);
+    IgnoreError(AddOrUpdate());
+
+exit:
+    return;
+}
+
+RoutingManager::LocalOmrPrefix::InfoString RoutingManager::LocalOmrPrefix::ToString(void) const
+{
+    InfoString string;
+
+    string.Append("local OMR prefix %s (def-route:%s)", mPrefix.ToString().AsCString(), ToYesNo(mDefaultRoute));
+    return string;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// OnLinkPrefixManager
+
+RoutingManager::OnLinkPrefixManager::OnLinkPrefixManager(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mState(kIdle)
+    , mTimer(aInstance)
+{
+    mLocalPrefix.Clear();
+    mFavoredDiscoveredPrefix.Clear();
+    mOldLocalPrefixes.Clear();
+}
+
+void RoutingManager::OnLinkPrefixManager::Init(void)
+{
+    TimeMilli                now = TimerMilli::GetNow();
+    Settings::BrOnLinkPrefix savedPrefix;
+    bool                     refreshStoredPrefixes = false;
+
+    // Restore old prefixes from `Settings`
+
+    for (int index = 0; Get<Settings>().ReadBrOnLinkPrefix(index, savedPrefix) == kErrorNone; index++)
+    {
+        uint32_t   lifetime;
+        OldPrefix *entry;
+
+        if (mOldLocalPrefixes.ContainsMatching(savedPrefix.GetPrefix()))
+        {
+            // We should not see duplicate entries in `Settings`
+            // but if we do we refresh the stored prefixes to make
+            // it consistent.
+            refreshStoredPrefixes = true;
+            continue;
+        }
+
+        entry = mOldLocalPrefixes.PushBack();
+
+        if (entry == nullptr)
+        {
+            // If there are more stored prefixes, we refresh the
+            // prefixes in `Settings` to remove the ones we cannot
+            // handle.
+
+            refreshStoredPrefixes = true;
+            break;
+        }
+
+        lifetime = Min(savedPrefix.GetLifetime(), Time::MsecToSec(TimerMilli::kMaxDelay));
+
+        entry->mPrefix     = savedPrefix.GetPrefix();
+        entry->mExpireTime = now + Time::SecToMsec(lifetime);
+
+        LogInfo("Restored old prefix %s, lifetime:%lu", entry->mPrefix.ToString().AsCString(), ToUlong(lifetime));
+
+        mTimer.FireAtIfEarlier(entry->mExpireTime);
+    }
+
+    if (refreshStoredPrefixes)
+    {
+        // We clear the entries in `Settings` and re-write the entries
+        // from `mOldLocalPrefixes` array.
+
+        IgnoreError(Get<Settings>().DeleteAllBrOnLinkPrefixes());
+
+        for (OldPrefix &oldPrefix : mOldLocalPrefixes)
+        {
+            SavePrefix(oldPrefix.mPrefix, oldPrefix.mExpireTime);
+        }
+    }
+
+    GenerateLocalPrefix();
+}
+
+void RoutingManager::OnLinkPrefixManager::GenerateLocalPrefix(void)
+{
+    const MeshCoP::ExtendedPanId &extPanId = Get<MeshCoP::ExtendedPanIdManager>().GetExtPanId();
+    OldPrefix                    *entry;
+    Ip6::Prefix                   oldLocalPrefix = mLocalPrefix;
+
+    // Global ID: 40 most significant bits of Extended PAN ID
+    // Subnet ID: 16 least significant bits of Extended PAN ID
+
+    mLocalPrefix.mPrefix.mFields.m8[0] = 0xfd;
+    memcpy(mLocalPrefix.mPrefix.mFields.m8 + 1, extPanId.m8, 5);
+    memcpy(mLocalPrefix.mPrefix.mFields.m8 + 6, extPanId.m8 + 6, 2);
+
+    mLocalPrefix.SetLength(kOnLinkPrefixLength);
+
+    // We ensure that the local prefix did change, since not all the
+    // bytes in Extended PAN ID are used in derivation of the local prefix.
+
+    VerifyOrExit(mLocalPrefix != oldLocalPrefix);
+
+    LogNote("Local on-link prefix: %s", mLocalPrefix.ToString().AsCString());
+
+    // Check if the new local prefix happens to be in `mOldLocalPrefixes` array.
+    // If so, we remove it from the array and set `mState` accordingly.
+
+    entry = mOldLocalPrefixes.FindMatching(mLocalPrefix);
+
+    if (entry != nullptr)
+    {
+        mState      = kDeprecating;
+        mExpireTime = entry->mExpireTime;
+        mOldLocalPrefixes.Remove(*entry);
+    }
+    else
+    {
+        mState = kIdle;
+    }
+
+exit:
+    return;
+}
+
+void RoutingManager::OnLinkPrefixManager::Start(void) {}
+
+void RoutingManager::OnLinkPrefixManager::Stop(void)
+{
+    mFavoredDiscoveredPrefix.Clear();
+
+    switch (mState)
+    {
+    case kIdle:
+        break;
+
+    case kPublishing:
+    case kAdvertising:
+    case kDeprecating:
+        mState = kDeprecating;
+        break;
+    }
+}
+
+void RoutingManager::OnLinkPrefixManager::Evaluate(void)
+{
+    VerifyOrExit(!Get<RoutingManager>().mRsSender.IsInProgress());
+
+    Get<RoutingManager>().mDiscoveredPrefixTable.FindFavoredOnLinkPrefix(mFavoredDiscoveredPrefix);
+
+    if ((mFavoredDiscoveredPrefix.GetLength() == 0) || (mFavoredDiscoveredPrefix == mLocalPrefix))
+    {
+        // We need to advertise our local on-link prefix when there is
+        // no discovered on-link prefix. If the favored discovered
+        // prefix is the same as our local on-link prefix we also
+        // start advertising the local prefix to add redundancy. Note
+        // that local on-link prefix is derived from extended PAN ID
+        // and therefore is the same for all BRs on the same Thread
+        // mesh.
+
+        PublishAndAdvertise();
+
+        // We remove the local on-link prefix from discovered prefix
+        // table, in case it was previously discovered and included in
+        // the table (now as a deprecating entry). We remove it with
+        // `kKeepInNetData` flag to ensure that the prefix is not
+        // unpublished from network data.
+        //
+        // Note that `ShouldProcessPrefixInfoOption()` will also check
+        // not allow the local on-link prefix to be added in the prefix
+        // table while we are advertising it.
+
+        Get<RoutingManager>().mDiscoveredPrefixTable.RemoveOnLinkPrefix(mLocalPrefix);
+
+        mFavoredDiscoveredPrefix.Clear();
+    }
+    else if (IsPublishingOrAdvertising())
+    {
+        // When an application-specific on-link prefix is received and
+        // it is larger than the local prefix, we will not remove the
+        // advertised local prefix. In this case, there will be two
+        // on-link prefixes on the infra link. But all BRs will still
+        // converge to the same smallest/favored on-link prefix and the
+        // application-specific prefix is not used.
+
+        if (!(mLocalPrefix < mFavoredDiscoveredPrefix))
+        {
+            LogInfo("EvaluateOnLinkPrefix: There is already favored on-link prefix %s",
+                    mFavoredDiscoveredPrefix.ToString().AsCString());
+            Deprecate();
+        }
+    }
+
+exit:
+    return;
+}
+
+bool RoutingManager::OnLinkPrefixManager::IsInitalEvaluationDone(void) const
+{
+    // This method indicates whether or not we are done with the
+    // initial policy evaluation of the on-link prefixes, i.e., either
+    // we have discovered a favored on-link prefix (being advertised by
+    // another router on infra link) or we are advertising our local
+    // on-link prefix.
+
+    return (mFavoredDiscoveredPrefix.GetLength() != 0 || IsPublishingOrAdvertising());
+}
+
+void RoutingManager::OnLinkPrefixManager::HandleDiscoveredPrefixTableChanged(void)
+{
+    // This is a callback from `mDiscoveredPrefixTable` indicating that
+    // there has been a change in the table. If the favored on-link
+    // prefix has changed, we trigger a re-evaluation of the routing
+    // policy.
+
+    Ip6::Prefix newFavoredPrefix;
+
+    Get<RoutingManager>().mDiscoveredPrefixTable.FindFavoredOnLinkPrefix(newFavoredPrefix);
+
+    if (newFavoredPrefix != mFavoredDiscoveredPrefix)
+    {
+        Get<RoutingManager>().ScheduleRoutingPolicyEvaluation(kAfterRandomDelay);
+    }
+}
+
+void RoutingManager::OnLinkPrefixManager::PublishAndAdvertise(void)
+{
+    // Start publishing and advertising the local on-link prefix if
+    // not already.
+
+    switch (mState)
+    {
+    case kIdle:
+    case kDeprecating:
+        break;
+
+    case kPublishing:
+    case kAdvertising:
+        ExitNow();
+    }
+
+    mState = kPublishing;
+    ResetExpireTime(TimerMilli::GetNow());
+
+    // 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.
+
+    if (Get<RoutingManager>().NetworkDataContainsUlaRoute())
+    {
+        EnterAdvertisingState();
+    }
+
+exit:
+    return;
+}
+
+void RoutingManager::OnLinkPrefixManager::Deprecate(void)
+{
+    // Deprecate the local on-link prefix if it was being advertised
+    // before. While depreciating the prefix, we wait for the lifetime
+    // timer to expire before unpublishing the prefix from the Network
+    // Data. We also continue to include it as a PIO in the RA message
+    // with zero preferred lifetime and the remaining valid lifetime
+    // until the timer expires.
+
+    switch (mState)
+    {
+    case kPublishing:
+    case kAdvertising:
+        mState = kDeprecating;
+        LogInfo("Deprecate local on-link prefix %s", mLocalPrefix.ToString().AsCString());
+        break;
+
+    case kIdle:
+    case kDeprecating:
+        break;
+    }
+}
+
+bool RoutingManager::OnLinkPrefixManager::ShouldPublishUlaRoute(void) const
+{
+    // 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.
+
+    return (mState != kIdle) || !mOldLocalPrefixes.IsEmpty();
+}
+
+void RoutingManager::OnLinkPrefixManager::ResetExpireTime(TimeMilli aNow)
+{
+    mExpireTime = aNow + TimeMilli::SecToMsec(kDefaultOnLinkPrefixLifetime);
+    mTimer.FireAtIfEarlier(mExpireTime);
+    SavePrefix(mLocalPrefix, mExpireTime);
+}
+
+void RoutingManager::OnLinkPrefixManager::EnterAdvertisingState(void)
+{
+    mState = kAdvertising;
+    LogInfo("Start advertising local on-link prefix %s", mLocalPrefix.ToString().AsCString());
+}
+
+bool RoutingManager::OnLinkPrefixManager::IsPublishingOrAdvertising(void) const
+{
+    return (mState == kPublishing) || (mState == kAdvertising);
+}
+
+void RoutingManager::OnLinkPrefixManager::AppendAsPiosTo(Ip6::Nd::RouterAdvertMessage &aRaMessage)
+{
+    AppendCurPrefix(aRaMessage);
+    AppendOldPrefixes(aRaMessage);
+}
+
+void RoutingManager::OnLinkPrefixManager::AppendCurPrefix(Ip6::Nd::RouterAdvertMessage &aRaMessage)
+{
+    // Append the local on-link prefix to the `aRaMessage` as a PIO
+    // only if it is being advertised or deprecated.
+    //
+    // If in `kAdvertising` state, we reset the expire time.
+    // If in `kDeprecating` state, we include it as PIO with zero
+    // preferred lifetime and the remaining valid lifetime.
+
+    uint32_t  validLifetime     = kDefaultOnLinkPrefixLifetime;
+    uint32_t  preferredLifetime = kDefaultOnLinkPrefixLifetime;
+    TimeMilli now               = TimerMilli::GetNow();
+
+    switch (mState)
+    {
+    case kAdvertising:
+        ResetExpireTime(now);
+        break;
+
+    case kDeprecating:
+        VerifyOrExit(mExpireTime > now);
+        validLifetime     = TimeMilli::MsecToSec(mExpireTime - now);
+        preferredLifetime = 0;
+        break;
+
+    case kIdle:
+    case kPublishing:
+        ExitNow();
+    }
+
+    SuccessOrAssert(aRaMessage.AppendPrefixInfoOption(mLocalPrefix, validLifetime, preferredLifetime));
+
+    LogInfo("RouterAdvert: Added PIO for %s (valid=%lu, preferred=%lu)", mLocalPrefix.ToString().AsCString(),
+            ToUlong(validLifetime), ToUlong(preferredLifetime));
+
+exit:
+    return;
+}
+
+void RoutingManager::OnLinkPrefixManager::AppendOldPrefixes(Ip6::Nd::RouterAdvertMessage &aRaMessage)
+{
+    TimeMilli now = TimerMilli::GetNow();
+    uint32_t  validLifetime;
+
+    for (const OldPrefix &oldPrefix : mOldLocalPrefixes)
+    {
+        if (oldPrefix.mExpireTime < now)
+        {
+            continue;
+        }
+
+        validLifetime = TimeMilli::MsecToSec(oldPrefix.mExpireTime - now);
+        SuccessOrAssert(aRaMessage.AppendPrefixInfoOption(oldPrefix.mPrefix, validLifetime, 0));
+
+        LogInfo("RouterAdvert: Added PIO for %s (valid=%lu, preferred=0)", oldPrefix.mPrefix.ToString().AsCString(),
+                ToUlong(validLifetime));
+    }
+}
+
+void RoutingManager::OnLinkPrefixManager::HandleNetDataChange(void)
+{
+    VerifyOrExit(mState == kPublishing);
+
+    if (Get<RoutingManager>().NetworkDataContainsUlaRoute())
+    {
+        EnterAdvertisingState();
+        Get<RoutingManager>().ScheduleRoutingPolicyEvaluation(kAfterRandomDelay);
+    }
+
+exit:
+    return;
+}
+
+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
+    // 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`
+    // device using addresses based on this prefix.
+
+    uint16_t    oldState  = mState;
+    Ip6::Prefix oldPrefix = mLocalPrefix;
+
+    GenerateLocalPrefix();
+
+    VerifyOrExit(oldPrefix != mLocalPrefix);
+
+    switch (oldState)
+    {
+    case kIdle:
+    case kPublishing:
+        break;
+
+    case kAdvertising:
+    case kDeprecating:
+        DeprecateOldPrefix(oldPrefix, mExpireTime);
+        break;
+    }
+
+    if (Get<RoutingManager>().mIsRunning)
+    {
+        Get<RoutingManager>().mRoutePublisher.Evaluate();
+        Get<RoutingManager>().ScheduleRoutingPolicyEvaluation(kAfterRandomDelay);
+    }
+
+exit:
+    return;
+}
+
+void RoutingManager::OnLinkPrefixManager::DeprecateOldPrefix(const Ip6::Prefix &aPrefix, TimeMilli aExpireTime)
+{
+    OldPrefix  *entry = nullptr;
+    Ip6::Prefix removedPrefix;
+
+    removedPrefix.Clear();
+
+    VerifyOrExit(!mOldLocalPrefixes.ContainsMatching(aPrefix));
+
+    LogInfo("Deprecating old on-link prefix %s", aPrefix.ToString().AsCString());
+
+    if (!mOldLocalPrefixes.IsFull())
+    {
+        entry = mOldLocalPrefixes.PushBack();
+    }
+    else
+    {
+        // If there is no more room in `mOldLocalPrefixes` array
+        // we evict the entry with the earliest expiration time.
+
+        entry = &mOldLocalPrefixes[0];
+
+        for (OldPrefix &oldPrefix : mOldLocalPrefixes)
+        {
+            if ((oldPrefix.mExpireTime < entry->mExpireTime))
+            {
+                entry = &oldPrefix;
+            }
+        }
+
+        removedPrefix = entry->mPrefix;
+
+        IgnoreError(Get<Settings>().RemoveBrOnLinkPrefix(removedPrefix));
+    }
+
+    entry->mPrefix     = aPrefix;
+    entry->mExpireTime = aExpireTime;
+    mTimer.FireAtIfEarlier(aExpireTime);
+
+    SavePrefix(aPrefix, aExpireTime);
+
+exit:
+    return;
+}
+
+void RoutingManager::OnLinkPrefixManager::SavePrefix(const Ip6::Prefix &aPrefix, TimeMilli aExpireTime)
+{
+    Settings::BrOnLinkPrefix savedPrefix;
+
+    savedPrefix.SetPrefix(aPrefix);
+    savedPrefix.SetLifetime(TimeMilli::MsecToSec(aExpireTime - TimerMilli::GetNow()));
+    IgnoreError(Get<Settings>().AddOrUpdateBrOnLinkPrefix(savedPrefix));
+}
+
+void RoutingManager::OnLinkPrefixManager::HandleTimer(void)
+{
+    TimeMilli                           now            = TimerMilli::GetNow();
+    TimeMilli                           nextExpireTime = now.GetDistantFuture();
+    Array<Ip6::Prefix, kMaxOldPrefixes> expiredPrefixes;
+
+    switch (mState)
+    {
+    case kIdle:
+        break;
+    case kPublishing:
+    case kAdvertising:
+    case kDeprecating:
+        if (now >= mExpireTime)
+        {
+            LogInfo("Local on-link prefix %s expired", mLocalPrefix.ToString().AsCString());
+            IgnoreError(Get<Settings>().RemoveBrOnLinkPrefix(mLocalPrefix));
+            mState = kIdle;
+        }
+        else
+        {
+            nextExpireTime = mExpireTime;
+        }
+        break;
+    }
+
+    for (OldPrefix &entry : mOldLocalPrefixes)
+    {
+        if (now >= entry.mExpireTime)
+        {
+            SuccessOrAssert(expiredPrefixes.PushBack(entry.mPrefix));
+        }
+        else
+        {
+            nextExpireTime = Min(nextExpireTime, entry.mExpireTime);
+        }
+    }
+
+    for (const Ip6::Prefix &prefix : expiredPrefixes)
+    {
+        LogInfo("Old local on-link prefix %s expired", prefix.ToString().AsCString());
+        IgnoreError(Get<Settings>().RemoveBrOnLinkPrefix(prefix));
+        mOldLocalPrefixes.RemoveMatching(prefix);
+    }
+
+    if (nextExpireTime != now.GetDistantFuture())
+    {
+        mTimer.FireAtIfEarlier(nextExpireTime);
+    }
+
+    Get<RoutingManager>().mRoutePublisher.Evaluate();
+}
+
 //---------------------------------------------------------------------------------------------------------------------
 // OnMeshPrefixArray
 
@@ -2161,6 +2610,495 @@
     }
 }
 
+//---------------------------------------------------------------------------------------------------------------------
+// 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>().mFavoredOmrPrefix.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>().mLocalOmrPrefix.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
+
+//---------------------------------------------------------------------------------------------------------------------
+// Nat64PrefixManager
+
+RoutingManager::Nat64PrefixManager::Nat64PrefixManager(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mEnabled(false)
+    , mTimer(aInstance)
+{
+    mInfraIfPrefix.Clear();
+    mLocalPrefix.Clear();
+    mPublishedPrefix.Clear();
+}
+
+void RoutingManager::Nat64PrefixManager::SetEnabled(bool aEnabled)
+{
+    VerifyOrExit(mEnabled != aEnabled);
+    mEnabled = aEnabled;
+
+    if (aEnabled)
+    {
+        if (Get<RoutingManager>().IsRunning())
+        {
+            Start();
+        }
+    }
+    else
+    {
+        Stop();
+    }
+
+exit:
+    return;
+}
+
+void RoutingManager::Nat64PrefixManager::Start(void)
+{
+    VerifyOrExit(mEnabled);
+    LogInfo("Starting Nat64PrefixManager");
+    mTimer.Start(0);
+
+exit:
+    return;
+}
+
+void RoutingManager::Nat64PrefixManager::Stop(void)
+{
+    LogInfo("Stopping Nat64PrefixManager");
+
+    if (mPublishedPrefix.IsValidNat64())
+    {
+        IgnoreError(Get<NetworkData::Publisher>().UnpublishPrefix(mPublishedPrefix));
+    }
+
+    mPublishedPrefix.Clear();
+    mInfraIfPrefix.Clear();
+    mTimer.Stop();
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    Get<Nat64::Translator>().ClearNat64Prefix();
+#endif
+}
+
+void RoutingManager::Nat64PrefixManager::GenerateLocalPrefix(const Ip6::Prefix &aBrUlaPrefix)
+{
+    mLocalPrefix = aBrUlaPrefix;
+    mLocalPrefix.SetSubnetId(kNat64PrefixSubnetId);
+    mLocalPrefix.mPrefix.mFields.m32[2] = 0;
+    mLocalPrefix.SetLength(kNat64PrefixLength);
+
+    LogInfo("Generated local NAT64 prefix: %s", mLocalPrefix.ToString().AsCString());
+}
+
+const Ip6::Prefix &RoutingManager::Nat64PrefixManager::GetFavoredPrefix(RoutePreference &aPreference) const
+{
+    const Ip6::Prefix *favoredPrefix = &mInfraIfPrefix;
+
+    if (mInfraIfPrefix.IsValidNat64())
+    {
+        aPreference = NetworkData::kRoutePreferenceMedium;
+    }
+    else
+    {
+        favoredPrefix = &mLocalPrefix;
+        aPreference   = NetworkData::kRoutePreferenceLow;
+    }
+
+    return *favoredPrefix;
+}
+
+void RoutingManager::Nat64PrefixManager::Evaluate(void)
+{
+    Error                            error;
+    Ip6::Prefix                      prefix;
+    RoutePreference                  preference;
+    NetworkData::ExternalRouteConfig netdataPrefixConfig;
+    bool                             shouldPublish;
+
+    VerifyOrExit(mEnabled);
+
+    LogInfo("Evaluating NAT64 prefix");
+
+    prefix = GetFavoredPrefix(preference);
+
+    error = Get<NetworkData::Leader>().GetPreferredNat64Prefix(netdataPrefixConfig);
+
+    // NAT64 prefix is expected to be published from this BR
+    // when one of the following is true:
+    //
+    // - No NAT64 prefix in Network Data.
+    // - The preferred NAT64 prefix in Network Data has lower
+    //   preference than this BR's prefix.
+    // - The preferred NAT64 prefix in Network Data was published
+    //   by this BR.
+    // - The preferred NAT64 prefix in Network Data is same as the
+    //   discovered infrastructure prefix.
+    //
+    // TODO: change to check RLOC16 to determine if the NAT64 prefix
+    // was published by this BR.
+
+    shouldPublish =
+        ((error == kErrorNotFound) || (netdataPrefixConfig.mPreference < preference) ||
+         (netdataPrefixConfig.GetPrefix() == mPublishedPrefix) || (netdataPrefixConfig.GetPrefix() == mInfraIfPrefix));
+
+    if (mPublishedPrefix.IsValidNat64() && (!shouldPublish || (prefix != mPublishedPrefix)))
+    {
+        IgnoreError(Get<NetworkData::Publisher>().UnpublishPrefix(mPublishedPrefix));
+        mPublishedPrefix.Clear();
+    }
+
+    if (shouldPublish && ((prefix != mPublishedPrefix) || (preference != mPublishedPreference)))
+    {
+        mPublishedPrefix     = prefix;
+        mPublishedPreference = preference;
+        Publish();
+    }
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    // When there is an prefix other than mLocalPrefix, means there is an external translator available. So we bypass
+    // the NAT64 translator by clearing the NAT64 prefix in the translator.
+    if (mPublishedPrefix == mLocalPrefix)
+    {
+        Get<Nat64::Translator>().SetNat64Prefix(mLocalPrefix);
+    }
+    else
+    {
+        Get<Nat64::Translator>().ClearNat64Prefix();
+    }
+#endif
+
+exit:
+    return;
+}
+
+void RoutingManager::Nat64PrefixManager::Publish(void)
+{
+    NetworkData::ExternalRouteConfig routeConfig;
+
+    routeConfig.Clear();
+    routeConfig.SetPrefix(mPublishedPrefix);
+    routeConfig.mPreference = mPublishedPreference;
+    routeConfig.mStable     = true;
+    routeConfig.mNat64      = true;
+
+    SuccessOrAssert(
+        Get<NetworkData::Publisher>().PublishExternalRoute(routeConfig, NetworkData::Publisher::kFromRoutingManager));
+}
+
+void RoutingManager::Nat64PrefixManager::HandleTimer(void)
+{
+    OT_ASSERT(mEnabled);
+
+    Discover();
+
+    mTimer.Start(TimeMilli::SecToMsec(kDefaultNat64PrefixLifetime));
+    LogInfo("NAT64 prefix timer scheduled in %lu seconds", ToUlong(kDefaultNat64PrefixLifetime));
+}
+
+void RoutingManager::Nat64PrefixManager::Discover(void)
+{
+    Error error = Get<RoutingManager>().mInfraIf.DiscoverNat64Prefix();
+
+    if (error == kErrorNone)
+    {
+        LogInfo("Discovering infraif NAT64 prefix");
+    }
+    else
+    {
+        LogWarn("Failed to discover infraif NAT64 prefix: %s", ErrorToString(error));
+    }
+}
+
+void RoutingManager::Nat64PrefixManager::HandleDiscoverDone(const Ip6::Prefix &aPrefix)
+{
+    mInfraIfPrefix = aPrefix;
+
+    LogInfo("Infraif NAT64 prefix: %s", mInfraIfPrefix.IsValidNat64() ? mInfraIfPrefix.ToString().AsCString() : "none");
+
+    if (Get<RoutingManager>().mIsRunning)
+    {
+        Get<RoutingManager>().ScheduleRoutingPolicyEvaluation(kAfterRandomDelay);
+    }
+}
+
+Nat64::State RoutingManager::Nat64PrefixManager::GetState(void) const
+{
+    Nat64::State state = Nat64::kStateDisabled;
+
+    VerifyOrExit(mEnabled);
+    VerifyOrExit(Get<RoutingManager>().IsRunning(), state = Nat64::kStateNotRunning);
+    VerifyOrExit(mPublishedPrefix.IsValidNat64(), state = Nat64::kStateIdle);
+    state = Nat64::kStateActive;
+
+exit:
+    return state;
+}
+
+#endif // OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+
+//---------------------------------------------------------------------------------------------------------------------
+// RsSender
+
+RoutingManager::RsSender::RsSender(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mTxCount(0)
+    , mTimer(aInstance)
+{
+}
+
+void RoutingManager::RsSender::Start(void)
+{
+    uint32_t delay;
+
+    VerifyOrExit(!IsInProgress());
+
+    delay = Random::NonCrypto::GetUint32InRange(0, kMaxStartDelay);
+    LogInfo("Scheduled Router Solicitation in %lu milliseconds", ToUlong(delay));
+
+    mTxCount   = 0;
+    mStartTime = TimerMilli::GetNow();
+    mTimer.Start(delay);
+
+exit:
+    return;
+}
+
+void RoutingManager::RsSender::Stop(void) { mTimer.Stop(); }
+
+Error RoutingManager::RsSender::SendRs(void)
+{
+    Ip6::Address                  destAddress;
+    Ip6::Nd::RouterSolicitMessage routerSolicit;
+    InfraIf::Icmp6Packet          packet;
+    Error                         error;
+
+    packet.InitFrom(routerSolicit);
+    destAddress.SetToLinkLocalAllRoutersMulticast();
+
+    error = Get<RoutingManager>().mInfraIf.Send(packet, destAddress);
+
+    if (error == kErrorNone)
+    {
+        Get<Ip6::Ip6>().GetBorderRoutingCounters().mRsTxSuccess++;
+    }
+    else
+    {
+        Get<Ip6::Ip6>().GetBorderRoutingCounters().mRsTxFailure++;
+    }
+    return error;
+}
+
+void RoutingManager::RsSender::HandleTimer(void)
+{
+    Error    error;
+    uint32_t delay;
+
+    if (mTxCount >= kMaxTxCount)
+    {
+        Get<RoutingManager>().HandleRsSenderFinished(mStartTime);
+        ExitNow();
+    }
+
+    error = SendRs();
+
+    if (error == kErrorNone)
+    {
+        mTxCount++;
+        LogInfo("Successfully sent RS %u/%u", mTxCount, kMaxTxCount);
+        delay = (mTxCount == kMaxTxCount) ? kWaitOnLastAttempt : kTxInterval;
+    }
+    else
+    {
+        LogCrit("Failed to send RS %u, error:%s", mTxCount + 1, ErrorToString(error));
+
+        // Note that `mTxCount` is intentionally not incremented
+        // if the tx fails.
+        delay = kRetryDelay;
+    }
+
+    mTimer.Start(delay);
+
+exit:
+    return;
+}
+
 } // namespace BorderRouter
 
 } // namespace ot
diff --git a/src/core/border_router/routing_manager.hpp b/src/core/border_router/routing_manager.hpp
index fcc07c4..65a01ff 100644
--- a/src/core/border_router/routing_manager.hpp
+++ b/src/core/border_router/routing_manager.hpp
@@ -47,6 +47,7 @@
 #error "OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE is required for OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE."
 #endif
 
+#include <openthread/nat64.h>
 #include <openthread/netdata.h>
 
 #include "border_router/infra_if.hpp"
@@ -59,6 +60,7 @@
 #include "common/string.hpp"
 #include "common/timer.hpp"
 #include "net/ip6.hpp"
+#include "net/nat64_translator.hpp"
 #include "net/nd6.hpp"
 #include "thread/network_data.hpp"
 
@@ -84,6 +86,32 @@
     typedef otBorderRoutingPrefixTableEntry    PrefixTableEntry;    ///< Prefix Table Entry.
 
     /**
+     * This constant specifies the maximum number of route prefixes that may be published by `RoutingManager`
+     * in Thread Network Data.
+     *
+     * This is used by `NetworkData::Publisher` to reserve entries for use by `RoutingManager`.
+     *
+     * The number of published entries accounts for:
+     * - Route prefix `fc00::/7` or `::/0`
+     * - One entry for NAT64 published prefix.
+     * - One extra entry for transitions.
+     *
+     */
+    static constexpr uint16_t kMaxPublishedPrefixes = 3;
+
+    /**
+     * This enumeration represents the states of `RoutingManager`.
+     *
+     */
+    enum State : uint8_t
+    {
+        kStateUninitialized = OT_BORDER_ROUTING_STATE_UNINITIALIZED, ///< Uninitialized.
+        kStateDisabled      = OT_BORDER_ROUTING_STATE_DISABLED,      ///< Initialized but disabled.
+        kStateStopped       = OT_BORDER_ROUTING_STATE_STOPPED,       ///< Initialized & enabled, but currently stopped.
+        kStateRunning       = OT_BORDER_ROUTING_STATE_RUNNING,       ///< Initialized, enabled, and running.
+    };
+
+    /**
      * This constructor initializes the routing manager.
      *
      * @param[in]  aInstance  A OpenThread instance.
@@ -118,22 +146,61 @@
     Error SetEnabled(bool aEnabled);
 
     /**
-     * This method gets the preference used when advertising Route Info Options (e.g., for discovered OMR prefixes) in
-     * Router Advertisement messages sent over the infrastructure link.
+     * This method indicates whether or not it is currently running.
      *
-     * @returns The Route Info Option preference.
+     * In order for the `RoutingManager` to be running it needs to be initialized and enabled, and device being
+     * attached.
+     *
+     * @retval TRUE  The RoutingManager is currently running.
+     * @retval FALSE The RoutingManager is not running.
      *
      */
-    RoutePreference GetRouteInfoOptionPreference(void) const { return mRouteInfoOptionPreference; }
+    bool IsRunning(void) const { return mIsRunning; }
 
     /**
-     * This method sets the preference to use when advertising Route Info Options (e.g., for discovered OMR prefixes)
-     * in Router Advertisement messages sent over the infrastructure link.
+     * This method gets the state of `RoutingManager`.
      *
-     * By default BR will use 'medium' preference level but this method allows the default value to be changed. As an
-     * example, it can be set to 'low' preference in the case where device is a temporary BR (a mobile BR or a
-     * battery-powered BR) to indicate that other BRs (if any) should be preferred over this BR on the infrastructure
-     * link.
+     * @returns The current state of `RoutingManager`.
+     *
+     */
+    State GetState(void) const;
+
+    /**
+     * This method requests the Border Routing Manager to stop.
+     *
+     * If Border Routing Manager is running, calling this method immediately stops it and triggers the preparation
+     * and sending of a final Router Advertisement (RA) message on infrastructure interface which deprecates and/or
+     * removes any previously advertised PIO/RIO prefixes. If Routing Manager is not running (or not enabled), no
+     * action is taken.
+     *
+     * Note that this method does not change whether the Routing Manager is enabled or disabled (see `SetEnabled()`).
+     * It stops the Routing Manager temporarily. After calling this method if the device role gets changes (device
+     * gets attached) and/or the infra interface state gets changed, the Routing Manager may be started again.
+     *
+     */
+    void RequestStop(void) { Stop(); }
+
+    /**
+     * This method gets the current preference used when advertising Route Info Options (RIO) in Router Advertisement
+     * messages sent over the infrastructure link.
+     *
+     * The RIO preference is determined as follows:
+     *
+     * - If explicitly set by user by calling `SetRouteInfoOptionPreference()`, the given preference is used.
+     * - Otherwise, it is determined based on device's role: Medium preference when in router/leader role and low
+     *   preference when in child role.
+     *
+     * @returns The current Route Info Option preference.
+     *
+     */
+    RoutePreference GetRouteInfoOptionPreference(void) const { return mRioPreference; }
+
+    /**
+     * This method explicitly sets the preference to use when advertising Route Info Options (RIO) in Router
+     * Advertisement messages sent over the infrastructure link.
+     *
+     * After a call to this method, BR will use the given preference for all its advertised RIOs. The preference can be
+     * cleared by calling `ClearRouteInfoOptionPreference`()`.
      *
      * @param[in] aPreference   The route preference to use.
      *
@@ -141,6 +208,15 @@
     void SetRouteInfoOptionPreference(RoutePreference aPreference);
 
     /**
+     * This method clears a previously set preference value for advertised Route Info Options.
+     *
+     * After a call to this method, BR will use device role to determine the RIO preference: Medium preference when
+     * in router/leader role and low preference when in child role.
+     *
+     */
+    void ClearRouteInfoOptionPreference(void);
+
+    /**
      * This method returns the local off-mesh-routable (OMR) prefix.
      *
      * The randomly generated 64-bit prefix will be added to the Thread Network Data if there isn't already an OMR
@@ -152,7 +228,7 @@
      * @retval  kErrorNone          Successfully retrieved the OMR prefix.
      *
      */
-    Error GetOmrPrefix(Ip6::Prefix &aPrefix);
+    Error GetOmrPrefix(Ip6::Prefix &aPrefix) const;
 
     /**
      * This method returns the currently favored off-mesh-routable (OMR) prefix.
@@ -165,14 +241,14 @@
      * @param[out] aPrefix         A reference to output the favored prefix.
      * @param[out] aPreference     A reference to output the preference associated with the favored OMR prefix.
      *
-     * @retval  kErrorInvalidState  The Border Routing Manager is not initialized yet.
+     * @retval  kErrorInvalidState  The Border Routing Manager is not running yet.
      * @retval  kErrorNone          Successfully retrieved the OMR prefix.
      *
      */
-    Error GetFavoredOmrPrefix(Ip6::Prefix &aPrefix, RoutePreference &aPreference);
+    Error GetFavoredOmrPrefix(Ip6::Prefix &aPrefix, RoutePreference &aPreference) const;
 
     /**
-     * This method returns the on-link prefix for the adjacent  infrastructure link.
+     * This method returns the on-link prefix for the adjacent infrastructure link.
      *
      * The randomly generated 64-bit prefix will be advertised
      * on the infrastructure link if there isn't already a usable
@@ -181,18 +257,49 @@
      * @param[out]  aPrefix  A reference to where the prefix will be output to.
      *
      * @retval  kErrorInvalidState  The Border Routing Manager is not initialized yet.
-     * @retval  kErrorNone          Successfully retrieved the on-link prefix.
+     * @retval  kErrorNone          Successfully retrieved the local on-link prefix.
      *
      */
-    Error GetOnLinkPrefix(Ip6::Prefix &aPrefix);
+    Error GetOnLinkPrefix(Ip6::Prefix &aPrefix) const;
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
+    /**
+     * This method returns the favored on-link prefix for the adjacent infrastructure link.
+     *
+     * The favored prefix is either a discovered prefix on the infrastructure link or the local on-link prefix.
+     *
+     * @param[out]  aPrefix  A reference to where the prefix will be output to.
+     *
+     * @retval  kErrorInvalidState  The Border Routing Manager is not initialized yet.
+     * @retval  kErrorNone          Successfully retrieved the favored on-link prefix.
+     *
+     */
+    Error GetFavoredOnLinkPrefix(Ip6::Prefix &aPrefix) const;
+
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+    /**
+     * Gets the state of NAT64 prefix publishing.
+     *
+     * @retval  kStateDisabled   NAT64 is disabled.
+     * @retval  kStateNotRunning NAT64 is enabled, but is not running since routing manager is not running.
+     * @retval  kStateIdle       NAT64 is enabled, but the border router is not publishing a NAT64 prefix. Usually
+     *                           when there is another border router publishing a NAT64 prefix with higher
+     *                           priority.
+     * @retval  kStateActive     The Border router is publishing a NAT64 prefix.
+     *
+     */
+    Nat64::State GetNat64PrefixManagerState(void) const { return mNat64PrefixManager.GetState(); }
+
+    /**
+     * Enable or disable NAT64 prefix publishing.
+     *
+     * @param[in]  aEnabled   A boolean to enable/disable NAT64 prefix publishing.
+     *
+     */
+    void SetNat64PrefixManagerEnabled(bool aEnabled);
+
     /**
      * This method returns the local NAT64 prefix.
      *
-     * The local NAT64 prefix will be published in the Thread network
-     * if none exists.
-     *
      * @param[out]  aPrefix  A reference to where the prefix will be output to.
      *
      * @retval  kErrorInvalidState  The Border Routing Manager is not initialized yet.
@@ -200,7 +307,31 @@
      *
      */
     Error GetNat64Prefix(Ip6::Prefix &aPrefix);
-#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
+
+    /**
+     * This method returns the currently favored NAT64 prefix.
+     *
+     * The favored NAT64 prefix can be discovered from infrastructure link or can be the local NAT64 prefix.
+     *
+     * @param[out] aPrefix         A reference to output the favored prefix.
+     * @param[out] aPreference     A reference to output the preference associated with the favored prefix.
+     *
+     * @retval  kErrorInvalidState  The Border Routing Manager is not initialized yet.
+     * @retval  kErrorNone          Successfully retrieved the NAT64 prefix.
+     *
+     */
+    Error GetFavoredNat64Prefix(Ip6::Prefix &aPrefix, RoutePreference &aRoutePreference);
+
+    /**
+     * This method informs `RoutingManager` of the result of the discovery request of NAT64 prefix on infrastructure
+     * interface (`InfraIf::DiscoverNat64Prefix()`).
+     *
+     * @param[in]  aPrefix  The discovered NAT64 prefix on `InfraIf`.
+     *
+     */
+    void HandleDiscoverNat64PrefixDone(const Ip6::Prefix &aPrefix) { mNat64PrefixManager.HandleDiscoverDone(aPrefix); }
+
+#endif // OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
 
     /**
      * This method processes a received ICMPv6 message from the infrastructure interface.
@@ -220,23 +351,26 @@
     void HandleInfraIfStateChanged(void) { EvaluateState(); }
 
     /**
-     * This method checks if the on-mesh prefix configuration is a valid OMR prefix.
+     * This method checks whether the on-mesh prefix configuration is a valid OMR prefix.
      *
      * @param[in] aOnMeshPrefixConfig  The on-mesh prefix configuration to check.
      *
-     * @returns  Whether the on-mesh prefix configuration is a valid OMR prefix.
+     * @retval   TRUE    The prefix is a valid OMR prefix.
+     * @retval   FALSE   The prefix is not a valid OMR prefix.
      *
      */
     static bool IsValidOmrPrefix(const NetworkData::OnMeshPrefixConfig &aOnMeshPrefixConfig);
 
     /**
-     * This method checks if the OMR prefix is valid (i.e. GUA/ULA prefix with length being 64).
+     * This method checks whether a given prefix is a valid OMR prefix.
      *
-     * @param[in]  aOmrPrefix  The OMR prefix to check.
-     * @returns    Whether the OMR prefix is valid.
+     * @param[in]  aPrefix  The prefix to check.
+     *
+     * @retval   TRUE    The prefix is a valid OMR prefix.
+     * @retval   FALSE   The prefix is not a valid OMR prefix.
      *
      */
-    static bool IsValidOmrPrefix(const Ip6::Prefix &aOmrPrefix);
+    static bool IsValidOmrPrefix(const Ip6::Prefix &aPrefix);
 
     /**
      * This method initializes a `PrefixTableIterator`.
@@ -269,6 +403,16 @@
         return mDiscoveredPrefixTable.GetNextEntry(aIterator, aEntry);
     }
 
+#if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
+    /**
+     * This method determines whether to enable/disable SRP server when the auto-enable mode is changed on SRP server.
+     *
+     * This should be called from `Srp::Server` when auto-enable mode is changed.
+     *
+     */
+    void HandleSrpServerAutoEnableMode(void);
+#endif
+
 private:
     static constexpr uint8_t kMaxOnMeshPrefixes = OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_ON_MESH_PREFIXES;
 
@@ -283,21 +427,16 @@
     // The maximum number of initial Router Advertisements.
     static constexpr uint32_t kMaxInitRtrAdvertisements = 3;
 
-    // The maximum number of Router Solicitations before sending Router Advertisements.
-    static constexpr uint32_t kMaxRtrSolicitations = 3;
-
     static constexpr uint32_t kDefaultOmrPrefixLifetime    = 1800; // The default OMR prefix valid lifetime. In sec.
     static constexpr uint32_t kDefaultOnLinkPrefixLifetime = 1800; // The default on-link prefix valid lifetime. In sec.
+    static constexpr uint32_t kDefaultNat64PrefixLifetime  = 300;  // The default NAT64 prefix valid lifetime. In sec.
     static constexpr uint32_t kMaxRtrAdvInterval           = 600;  // Max Router Advertisement Interval. In sec.
     static constexpr uint32_t kMinRtrAdvInterval           = kMaxRtrAdvInterval / 3; // Min RA Interval. In sec.
     static constexpr uint32_t kMaxInitRtrAdvInterval       = 16;                     // Max Initial RA Interval. In sec.
-    static constexpr uint32_t kRaReplyJitter               = 500;    // Jitter for sending RA after rx RS. In msec.
-    static constexpr uint32_t kRtrSolicitationInterval     = 4;      // Interval between RSs. In sec.
-    static constexpr uint32_t kMaxRtrSolicitationDelay     = 1;      // Max delay for initial solicitation. In sec.
-    static constexpr uint32_t kRoutingPolicyEvaluationJitter = 1000; // Jitter for routing policy evaluation. In msec.
-    static constexpr uint32_t kRtrSolicitationRetryDelay =
-        kRtrSolicitationInterval;                             // The delay before retrying failed RS tx. In Sec.
-    static constexpr uint32_t kMinDelayBetweenRtrAdvs = 3000; // Min delay (msec) between consecutive RAs.
+    static constexpr uint32_t kRaReplyJitter               = 500;  // Jitter for sending RA after rx RS. In msec.
+    static constexpr uint32_t kPolicyEvaluationMinDelay    = 2000; // Min delay for policy evaluation. In msec.
+    static constexpr uint32_t kPolicyEvaluationMaxDelay    = 4000; // Max delay for policy evaluation. In msec.
+    static constexpr uint32_t kMinDelayBetweenRtrAdvs      = 3000; // Min delay (msec) between consecutive RAs.
 
     // The STALE_RA_TIME in seconds. The Routing Manager will consider the prefixes
     // and learned RA parameters STALE when they are not refreshed in STALE_RA_TIME
@@ -312,6 +451,8 @@
     static_assert(kDefaultOnLinkPrefixLifetime >= kMaxRtrAdvInterval, "invalid default on-link prefix lifetime");
     static_assert(kRtrAdvStaleTime >= 1800 && kRtrAdvStaleTime <= kDefaultOnLinkPrefixLifetime,
                   "invalid RA STALE time");
+    static_assert(kPolicyEvaluationMaxDelay > kPolicyEvaluationMinDelay,
+                  "kPolicyEvaluationMaxDelay must be larger than kPolicyEvaluationMinDelay");
 
     enum RouterAdvTxMode : uint8_t // Used in `SendRouterAdvertisement()`
     {
@@ -319,6 +460,18 @@
         kAdvPrefixesFromNetData,
     };
 
+    enum ScheduleMode : uint8_t // Used in `ScheduleRoutingPolicyEvaluation()`
+    {
+        kImmediately,
+        kForNextRa,
+        kAfterRandomDelay,
+        kToReplyToRs,
+    };
+
+    void HandleDiscoveredPrefixTableChanged(void); // Declare early so we can use in `mSignalTask`
+    void HandleDiscoveredPrefixTableEntryTimer(void) { mDiscoveredPrefixTable.HandleEntryTimer(); }
+    void HandleDiscoveredPrefixTableRouterTimer(void) { mDiscoveredPrefixTable.HandleRouterTimer(); }
+
     class DiscoveredPrefixTable : public InstanceLocator
     {
         // This class maintains the discovered on-link and route prefixes
@@ -340,25 +493,20 @@
         // invoked after all the changes are processed.
 
     public:
-        enum NetDataMode : uint8_t // Used in `Remove{}` methods
-        {
-            kUnpublishFromNetData, // Unpublish the entry from Network Data if previously published.
-            kKeepInNetData,        // Keep entry in Network Data if previously published.
-        };
-
         explicit DiscoveredPrefixTable(Instance &aInstance);
 
         void ProcessRouterAdvertMessage(const Ip6::Nd::RouterAdvertMessage &aRaMessage,
-                                        const Ip6::Address &                aSrcAddress);
+                                        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, NetDataMode aNetDataMode);
 
-        bool ContainsRoutePrefix(const Ip6::Prefix &aPrefix) const;
-        void RemoveRoutePrefix(const Ip6::Prefix &aPrefix, NetDataMode aNetDataMode);
+        void RemoveOnLinkPrefix(const Ip6::Prefix &aPrefix);
+        void RemoveRoutePrefix(const Ip6::Prefix &aPrefix);
 
         void RemoveAllEntries(void);
         void RemoveOrDeprecateOldEntries(TimeMilli aTimeThreshold);
@@ -368,6 +516,9 @@
         void  InitIterator(PrefixTableIterator &aIterator) const;
         Error GetNextEntry(PrefixTableIterator &aIterator, PrefixTableEntry &aEntry) const;
 
+        void HandleEntryTimer(void);
+        void HandleRouterTimer(void);
+
     private:
         static constexpr uint16_t kMaxRouters = OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_ROUTERS;
         static constexpr uint16_t kMaxEntries = OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_PREFIXES;
@@ -375,6 +526,7 @@
         class Entry : public LinkedListEntry<Entry>, public Unequatable<Entry>, private Clearable<Entry>
         {
             friend class LinkedListEntry<Entry>;
+            friend class Clearable<Entry>;
 
         public:
             enum Type : uint8_t
@@ -392,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
@@ -410,8 +581,9 @@
             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; }
+            const TimeMilli   &GetLastUpdateTime(void) const { return mLastUpdateTime; }
             uint32_t           GetValidLifetime(void) const { return mValidLifetime; }
             void               ClearValidLifetime(void) { mValidLifetime = 0; }
             TimeMilli          GetExpireTime(void) const;
@@ -419,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; }
@@ -433,7 +606,7 @@
         private:
             static uint32_t CalculateExpireDelay(uint32_t aValidLifetime);
 
-            Entry *     mNext;
+            Entry      *mNext;
             Ip6::Prefix mPrefix;
             Type        mType;
             TimeMilli   mLastUpdateTime;
@@ -447,6 +620,17 @@
 
         struct Router
         {
+            // The timeout (in msec) for router staying in active state
+            // before starting the Neighbor Solicitation (NS) probes.
+            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 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");
+
             enum EmptyChecker : uint8_t
             {
                 kContainsNoEntries
@@ -457,6 +641,8 @@
 
             Ip6::Address      mAddress;
             LinkedList<Entry> mEntries;
+            TimeMilli         mTimeout;
+            uint8_t           mNsProbeCount;
         };
 
         class Iterator : public PrefixTableIterator
@@ -464,36 +650,38 @@
         public:
             const Router *GetRouter(void) const { return static_cast<const Router *>(mPtr1); }
             void          SetRouter(const Router *aRouter) { mPtr1 = aRouter; }
-            const Entry * GetEntry(void) const { return static_cast<const Entry *>(mPtr2); }
+            const Entry  *GetEntry(void) const { return static_cast<const Entry *>(mPtr2); }
             void          SetEntry(const Entry *aEntry) { mPtr2 = aEntry; }
             TimeMilli     GetInitTime(void) const { return TimeMilli(mData32); }
             void          SetInitTime(void) { mData32 = TimerMilli::GetNow().GetValue(); }
         };
 
-        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;
-        void        RemovePrefix(const Entry::Matcher &aMatcher, NetDataMode aNetDataMode);
-        void        RemoveRoutersWithNoEntries(void);
-        Entry *     AllocateEntry(void) { return mEntryPool.Allocate(); }
-        void        FreeEntry(Entry &aEntry) { mEntryPool.Free(aEntry); }
-        void        FreeEntries(LinkedList<Entry> &aEntries);
-        void        UpdateNetworkDataOnChangeTo(Entry &aEntry);
-        Entry *     FindFavoredEntryToPublish(const Ip6::Prefix &aPrefix);
-        void        PublishEntry(const Entry &aEntry);
-        void        UnpublishEntry(const Entry &aEntry);
-        static void HandleTimer(Timer &aTimer);
-        void        HandleTimer(void);
-        void        RemoveExpiredEntries(void);
-        void        SignalTableChanged(void);
-        static void HandleSignalTask(Tasklet &aTasklet);
+        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         Contains(const Entry::Checker &aChecker) const;
+        void         RemovePrefix(const Entry::Matcher &aMatcher);
+        void         RemoveOrDeprecateEntriesFromInactiveRouters(void);
+        void         RemoveRoutersWithNoEntries(void);
+        Entry       *AllocateEntry(void) { return mEntryPool.Allocate(); }
+        void         FreeEntry(Entry &aEntry) { mEntryPool.Free(aEntry); }
+        void         FreeEntries(LinkedList<Entry> &aEntries);
+        void         UpdateNetworkDataOnChangeTo(Entry &aEntry);
+        const Entry *FindFavoredEntryToPublish(const Ip6::Prefix &aPrefix) const;
+        void         RemoveExpiredEntries(void);
+        void         SignalTableChanged(void);
+        void         UpdateRouterOnRx(Router &aRouter);
+        void         SendNeighborSolicitToRouter(const Router &aRouter);
+
+        using SignalTask  = TaskletIn<RoutingManager, &RoutingManager::HandleDiscoveredPrefixTableChanged>;
+        using EntryTimer  = TimerMilliIn<RoutingManager, &RoutingManager::HandleDiscoveredPrefixTableEntryTimer>;
+        using RouterTimer = TimerMilliIn<RoutingManager, &RoutingManager::HandleDiscoveredPrefixTableRouterTimer>;
 
         Array<Router, kMaxRouters> mRouters;
         Pool<Entry, kMaxEntries>   mEntryPool;
-        TimerMilli                 mTimer;
-        Tasklet                    mSignalTask;
-        bool                       mAllowDefaultRouteInNetData;
+        EntryTimer                 mEntryTimer;
+        RouterTimer                mRouterTimer;
+        SignalTask                 mSignalTask;
     };
 
     class LocalOmrPrefix;
@@ -504,18 +692,21 @@
         OmrPrefix(void) { Clear(); }
 
         bool               IsEmpty(void) const { return (mPrefix.GetLength() == 0); }
+        bool               IsInfrastructureDerived(void) const;
         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:
         Ip6::Prefix     mPrefix;
         RoutePreference mPreference;
+        bool            mIsDomainPrefix;
     };
 
-    class LocalOmrPrefix : InstanceLocator
+    class LocalOmrPrefix : public InstanceLocator
     {
     public:
         explicit LocalOmrPrefix(Instance &aInstance);
@@ -525,10 +716,81 @@
         Error              AddToNetData(void);
         void               RemoveFromNetData(void);
         bool               IsAddedInNetData(void) const { return mIsAddedInNetData; }
+        void               UpdateDefaultRouteFlag(bool aDefaultRoute);
 
     private:
+        static constexpr uint16_t kInfoStringSize = 85;
+
+        typedef String<kInfoStringSize> InfoString;
+
+        Error      AddOrUpdate(void);
+        InfoString ToString(void) const;
+
         Ip6::Prefix mPrefix;
         bool        mIsAddedInNetData;
+        bool        mDefaultRoute;
+    };
+
+    void HandleOnLinkPrefixManagerTimer(void) { mOnLinkPrefixManager.HandleTimer(); }
+
+    class OnLinkPrefixManager : public InstanceLocator
+    {
+    public:
+        explicit OnLinkPrefixManager(Instance &aInstance);
+
+        // Max number of old on-link prefixes to retain to deprecate.
+        static constexpr uint16_t kMaxOldPrefixes = OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_OLD_ON_LINK_PREFIXES;
+
+        void               Init(void);
+        void               Start(void);
+        void               Stop(void);
+        void               Evaluate(void);
+        const Ip6::Prefix &GetLocalPrefix(void) const { return mLocalPrefix; }
+        const Ip6::Prefix &GetFavoredDiscoveredPrefix(void) const { return mFavoredDiscoveredPrefix; }
+        bool               IsInitalEvaluationDone(void) const;
+        void               HandleDiscoveredPrefixTableChanged(void);
+        bool               ShouldPublishUlaRoute(void) const;
+        void               AppendAsPiosTo(Ip6::Nd::RouterAdvertMessage &aRaMessage);
+        bool               IsPublishingOrAdvertising(void) const;
+        void               HandleNetDataChange(void);
+        void               HandleExtPanIdChange(void);
+        void               HandleTimer(void);
+
+    private:
+        enum State : uint8_t // State of `mLocalPrefix`
+        {
+            kIdle,
+            kPublishing,
+            kAdvertising,
+            kDeprecating,
+        };
+
+        struct OldPrefix
+        {
+            bool Matches(const Ip6::Prefix &aPrefix) const { return mPrefix == aPrefix; }
+
+            Ip6::Prefix mPrefix;
+            TimeMilli   mExpireTime;
+        };
+
+        void GenerateLocalPrefix(void);
+        void PublishAndAdvertise(void);
+        void Deprecate(void);
+        void ResetExpireTime(TimeMilli aNow);
+        void EnterAdvertisingState(void);
+        void AppendCurPrefix(Ip6::Nd::RouterAdvertMessage &aRaMessage);
+        void AppendOldPrefixes(Ip6::Nd::RouterAdvertMessage &aRaMessage);
+        void DeprecateOldPrefix(const Ip6::Prefix &aPrefix, TimeMilli aExpireTime);
+        void SavePrefix(const Ip6::Prefix &aPrefix, TimeMilli aExpireTime);
+
+        using ExpireTimer = TimerMilliIn<RoutingManager, &RoutingManager::HandleOnLinkPrefixManagerTimer>;
+
+        Ip6::Prefix                       mLocalPrefix;
+        State                             mState;
+        TimeMilli                         mExpireTime;
+        Ip6::Prefix                       mFavoredDiscoveredPrefix;
+        Array<OldPrefix, kMaxOldPrefixes> mOldLocalPrefixes;
+        ExpireTimer                       mTimer;
     };
 
     typedef Ip6::Prefix OnMeshPrefix;
@@ -540,51 +802,180 @@
         void MarkAsDeleted(const OnMeshPrefix &aPrefix);
     };
 
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+    void HandleNat64PrefixManagerTimer(void) { mNat64PrefixManager.HandleTimer(); }
+
+    class Nat64PrefixManager : public InstanceLocator
+    {
+    public:
+        // This class manages the NAT64 related functions including
+        // generation of local NAT64 prefix, discovery of infra
+        // interface prefix, maintaining the discovered prefix
+        // lifetime, and selection of the NAT64 prefix to publish in
+        // Network Data.
+        //
+        // Calling methods except GenerateLocalPrefix and SetEnabled
+        // when disabled becomes no-op.
+
+        explicit Nat64PrefixManager(Instance &aInstance);
+
+        void         SetEnabled(bool aEnabled);
+        Nat64::State GetState(void) const;
+
+        void Start(void);
+        void Stop(void);
+
+        void               GenerateLocalPrefix(const Ip6::Prefix &aBrUlaPrefix);
+        const Ip6::Prefix &GetLocalPrefix(void) const { return mLocalPrefix; }
+        const Ip6::Prefix &GetFavoredPrefix(RoutePreference &aPreference) const;
+        void               Evaluate(void);
+        void               HandleDiscoverDone(const Ip6::Prefix &aPrefix);
+        void               HandleTimer(void);
+
+    private:
+        void Discover(void);
+        void Publish(void);
+
+        using Nat64Timer = TimerMilliIn<RoutingManager, &RoutingManager::HandleNat64PrefixManagerTimer>;
+
+        bool mEnabled;
+
+        Ip6::Prefix     mInfraIfPrefix;       // The latest NAT64 prefix discovered on the infrastructure interface.
+        Ip6::Prefix     mLocalPrefix;         // The local prefix (from BR ULA prefix).
+        Ip6::Prefix     mPublishedPrefix;     // The prefix to publish in Net Data (empty or local or from infra-if).
+        RoutePreference mPublishedPreference; // The published prefix preference.
+        Nat64Timer      mTimer;
+    };
+#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,
+        // last tx time, header to use and whether the header is
+        // discovered from receiving RAs from the host itself. This
+        // ensures that if an entity on host is advertising certain
+        // info in its RA header (e.g., a default route), the RAs we
+        // emit from `RoutingManager` also include the same header.
+
+        RaInfo(void)
+            : mHeaderUpdateTime(TimerMilli::GetNow())
+            , mIsHeaderFromHost(false)
+            , mTxCount(0)
+            , mLastTxTime(TimerMilli::GetNow() - kMinDelayBetweenRtrAdvs)
+        {
+        }
+
+        Ip6::Nd::RouterAdvertMessage::Header mHeader;
+        TimeMilli                            mHeaderUpdateTime;
+        bool                                 mIsHeaderFromHost;
+        uint32_t                             mTxCount;
+        TimeMilli                            mLastTxTime;
+    };
+
+    void HandleRsSenderTimer(void) { mRsSender.HandleTimer(); }
+
+    class RsSender : public InstanceLocator
+    {
+    public:
+        // This class implements tx of Router Solicitation (RS)
+        // messages to discover other routers. `Start()` schedules
+        // a cycle of RS transmissions of `kMaxTxCount` separated
+        // by `kTxInterval`. At the end of cycle the callback
+        // `HandleRsSenderFinished()` is invoked to inform end of
+        // the cycle to `RoutingManager`.
+
+        explicit RsSender(Instance &aInstance);
+
+        bool IsInProgress(void) const { return mTimer.IsRunning(); }
+        void Start(void);
+        void Stop(void);
+        void HandleTimer(void);
+
+    private:
+        // All time intervals are in msec.
+        static constexpr uint32_t kMaxStartDelay     = 1000;        // Max random delay to send the first RS.
+        static constexpr uint32_t kTxInterval        = 4000;        // Interval between RS tx.
+        static constexpr uint32_t kRetryDelay        = kTxInterval; // Interval to wait to retry a failed RS tx.
+        static constexpr uint32_t kWaitOnLastAttempt = 1000;        // Wait interval after last RS tx.
+        static constexpr uint8_t  kMaxTxCount        = 3;           // Number of RS tx in one cycle.
+
+        Error SendRs(void);
+
+        using RsTimer = TimerMilliIn<RoutingManager, &RoutingManager::HandleRsSenderTimer>;
+
+        uint8_t   mTxCount;
+        RsTimer   mTimer;
+        TimeMilli mStartTime;
+    };
+
     void  EvaluateState(void);
     void  Start(void);
     void  Stop(void);
     void  HandleNotifierEvents(Events aEvents);
     bool  IsInitialized(void) const { return mInfraIf.IsInitialized(); }
     bool  IsEnabled(void) const { return mIsEnabled; }
+    void  SetRioPreferenceBasedOnRole(void);
+    void  UpdateRioPreference(RoutePreference aPreference);
     Error LoadOrGenerateRandomBrUlaPrefix(void);
-    void  GenerateOnLinkPrefix(void);
 
-    void EvaluateOnLinkPrefix(void);
+    void EvaluateRoutingPolicy(void);
+    bool IsInitalPolicyEvaluationDone(void) const;
+    void ScheduleRoutingPolicyEvaluation(ScheduleMode aMode);
+    void DetermineFavoredOmrPrefix(void);
+    void EvaluateOmrPrefix(void);
+    void HandleRsSenderFinished(TimeMilli aStartTime);
+    void SendRouterAdvertisement(RouterAdvTxMode aRaTxMode);
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
-    void GenerateNat64Prefix(void);
-    void EvaluateNat64Prefix(void);
-#endif
+    void HandleDiscoveredPrefixStaleTimer(void);
 
-    void  EvaluateRoutingPolicy(void);
-    void  StartRoutingPolicyEvaluationJitter(uint32_t aJitterMilli);
-    void  StartRoutingPolicyEvaluationDelay(uint32_t aDelayMilli);
-    void  EvaluateOmrPrefix(void);
-    Error PublishExternalRoute(const Ip6::Prefix &aPrefix, RoutePreference aRoutePreference, bool aNat64 = false);
-    void  UnpublishExternalRoute(const Ip6::Prefix &aPrefix);
-    void  StartRouterSolicitationDelay(void);
-    Error SendRouterSolicitation(void);
-    void  SendRouterAdvertisement(RouterAdvTxMode aRaTxMode);
-    bool  IsRouterSolicitationInProgress(void) const;
-
-    static void HandleRouterSolicitTimer(Timer &aTimer);
-    void        HandleRouterSolicitTimer(void);
-    static void HandleDiscoveredPrefixInvalidTimer(Timer &aTimer);
-    void        HandleDiscoveredPrefixInvalidTimer(void);
-    static void HandleDiscoveredPrefixStaleTimer(Timer &aTimer);
-    void        HandleDiscoveredPrefixStaleTimer(void);
-    static void HandleRoutingPolicyTimer(Timer &aTimer);
-    void        HandleOnLinkPrefixDeprecateTimer(void);
-    static void HandleOnLinkPrefixDeprecateTimer(Timer &aTimer);
-
-    void DeprecateOnLinkPrefix(void);
-    void HandleRouterSolicit(const InfraIf::Icmp6Packet &aPacket, const Ip6::Address &aSrcAddress);
     void HandleRouterAdvertisement(const InfraIf::Icmp6Packet &aPacket, const Ip6::Address &aSrcAddress);
+    void HandleRouterSolicit(const InfraIf::Icmp6Packet &aPacket, const Ip6::Address &aSrcAddress);
+    void HandleNeighborAdvertisement(const InfraIf::Icmp6Packet &aPacket);
     bool ShouldProcessPrefixInfoOption(const Ip6::Nd::PrefixInfoOption &aPio, const Ip6::Prefix &aPrefix);
     bool ShouldProcessRouteInfoOption(const Ip6::Nd::RouteInfoOption &aRio, const Ip6::Prefix &aPrefix);
     void UpdateDiscoveredPrefixTableOnNetDataChange(void);
-    void HandleDiscoveredPrefixTableChanged(void);
     bool NetworkDataContainsOmrPrefix(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);
@@ -593,6 +984,9 @@
     static bool IsValidOnLinkPrefix(const Ip6::Nd::PrefixInfoOption &aPio);
     static bool IsValidOnLinkPrefix(const Ip6::Prefix &aOnLinkPrefix);
 
+    using RoutingPolicyTimer         = TimerMilliIn<RoutingManager, &RoutingManager::EvaluateRoutingPolicy>;
+    using DiscoveredPrefixStaleTimer = TimerMilliIn<RoutingManager, &RoutingManager::HandleDiscoveredPrefixStaleTimer>;
+
     // Indicates whether the Routing Manager is running (started).
     bool mIsRunning;
 
@@ -613,52 +1007,30 @@
     // were advertised as RIO in the last sent RA message.
     OnMeshPrefixArray mAdvertisedPrefixes;
 
-    RoutePreference mRouteInfoOptionPreference;
+    RoutePreference mRioPreference;
+    bool            mUserSetRioPreference;
 
-    // The currently favored (smallest) discovered on-link prefix.
-    // Prefix length of zero indicates there is none.
-    Ip6::Prefix mFavoredDiscoveredOnLinkPrefix;
-
-    // The on-link prefix loaded from local persistent storage or
-    // randomly generated if non is found in persistent storage.
-    Ip6::Prefix mLocalOnLinkPrefix;
-
-    bool mIsAdvertisingLocalOnLinkPrefix;
-
-    // The last time when the on-link prefix is advertised with
-    // non-zero preferred lifetime.
-    TimeMilli  mTimeAdvertisedOnLinkPrefix;
-    TimerMilli mOnLinkPrefixDeprecateTimer;
-
-    // The NAT64 prefix allocated from the /48 BR ULA prefix.
-    Ip6::Prefix mLocalNat64Prefix;
-
-    // True if the local NAT64 prefix is advertised in Thread network.
-    bool mIsAdvertisingLocalNat64Prefix;
+    OnLinkPrefixManager mOnLinkPrefixManager;
 
     DiscoveredPrefixTable mDiscoveredPrefixTable;
 
-    // The RA header and parameters for the infra interface.
-    // This value is initialized with `RouterAdvMessage::SetToDefault`
-    // and updated with RA messages initiated from infra interface.
-    Ip6::Nd::RouterAdvertMessage::Header mRouterAdvertHeader;
-    TimeMilli                            mTimeRouterAdvMessageLastUpdate;
-    bool                                 mLearntRouterAdvMessageFromHost;
+    RoutePublisher mRoutePublisher;
 
-    TimerMilli mDiscoveredPrefixStaleTimer;
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+    Nat64PrefixManager mNat64PrefixManager;
+#endif
 
-    uint32_t  mRouterAdvertisementCount;
-    TimeMilli mLastRouterAdvertisementSendTime;
+    RaInfo   mRaInfo;
+    RsSender mRsSender;
 
-    TimerMilli mRouterSolicitTimer;
-    TimeMilli  mTimeRouterSolicitStart;
-    uint8_t    mRouterSolicitCount;
-
-    TimerMilli mRoutingPolicyTimer;
+    DiscoveredPrefixStaleTimer mDiscoveredPrefixStaleTimer;
+    RoutingPolicyTimer         mRoutingPolicyTimer;
 };
 
 } // namespace BorderRouter
 
+DefineMapEnum(otBorderRoutingState, BorderRouter::RoutingManager::State);
+
 } // namespace ot
 
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
diff --git a/src/core/coap/coap.cpp b/src/core/coap/coap.cpp
index f0b966d..38e5618 100644
--- a/src/core/coap/coap.cpp
+++ b/src/core/coap/coap.cpp
@@ -54,11 +54,8 @@
     : InstanceLocator(aInstance)
     , mMessageId(Random::NonCrypto::GetUint16())
     , mRetransmissionTimer(aInstance, Coap::HandleRetransmissionTimer, this)
-    , mContext(nullptr)
-    , mInterceptor(nullptr)
     , mResponsesQueue(aInstance)
-    , mDefaultHandler(nullptr)
-    , mDefaultHandlerContext(nullptr)
+    , mResourceHandler(nullptr)
     , mSender(aSender)
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
     , mLastResponse(nullptr)
@@ -72,10 +69,7 @@
     mResponsesQueue.DequeueAllResponses();
 }
 
-void CoapBase::ClearRequests(const Ip6::Address &aAddress)
-{
-    ClearRequests(&aAddress);
-}
+void CoapBase::ClearRequests(const Ip6::Address &aAddress) { ClearRequests(&aAddress); }
 
 void CoapBase::ClearRequests(const Ip6::Address *aAddress)
 {
@@ -93,10 +87,7 @@
 }
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-void CoapBase::AddBlockWiseResource(ResourceBlockWise &aResource)
-{
-    IgnoreError(mBlockWiseResources.Add(aResource));
-}
+void CoapBase::AddBlockWiseResource(ResourceBlockWise &aResource) { IgnoreError(mBlockWiseResources.Add(aResource)); }
 
 void CoapBase::RemoveBlockWiseResource(ResourceBlockWise &aResource)
 {
@@ -105,10 +96,7 @@
 }
 #endif
 
-void CoapBase::AddResource(Resource &aResource)
-{
-    IgnoreError(mResources.Add(aResource));
-}
+void CoapBase::AddResource(Resource &aResource) { IgnoreError(mResources.Add(aResource)); }
 
 void CoapBase::RemoveResource(Resource &aResource)
 {
@@ -116,18 +104,6 @@
     aResource.SetNext(nullptr);
 }
 
-void CoapBase::SetDefaultHandler(RequestHandler aHandler, void *aContext)
-{
-    mDefaultHandler        = aHandler;
-    mDefaultHandlerContext = aContext;
-}
-
-void CoapBase::SetInterceptor(Interceptor aInterceptor, void *aContext)
-{
-    mInterceptor = aInterceptor;
-    mContext     = aContext;
-}
-
 Message *CoapBase::NewMessage(const Message::Settings &aSettings)
 {
     Message *message = nullptr;
@@ -139,24 +115,28 @@
     return message;
 }
 
-Message *CoapBase::NewPriorityConfirmablePostMessage(const char *aUriPath)
+Message *CoapBase::NewMessage(void) { return NewMessage(Message::Settings::GetDefault()); }
+
+Message *CoapBase::NewPriorityMessage(void)
 {
-    return InitMessage(NewPriorityMessage(), kTypeConfirmable, aUriPath);
+    return NewMessage(Message::Settings(Message::kWithLinkSecurity, Message::kPriorityNet));
 }
 
-Message *CoapBase::NewConfirmablePostMessage(const char *aUriPath)
+Message *CoapBase::NewPriorityConfirmablePostMessage(Uri aUri)
 {
-    return InitMessage(NewMessage(), kTypeConfirmable, aUriPath);
+    return InitMessage(NewPriorityMessage(), kTypeConfirmable, aUri);
 }
 
-Message *CoapBase::NewPriorityNonConfirmablePostMessage(const char *aUriPath)
+Message *CoapBase::NewConfirmablePostMessage(Uri aUri) { return InitMessage(NewMessage(), kTypeConfirmable, aUri); }
+
+Message *CoapBase::NewPriorityNonConfirmablePostMessage(Uri aUri)
 {
-    return InitMessage(NewPriorityMessage(), kTypeNonConfirmable, aUriPath);
+    return InitMessage(NewPriorityMessage(), kTypeNonConfirmable, aUri);
 }
 
-Message *CoapBase::NewNonConfirmablePostMessage(const char *aUriPath)
+Message *CoapBase::NewNonConfirmablePostMessage(Uri aUri)
 {
-    return InitMessage(NewMessage(), kTypeNonConfirmable, aUriPath);
+    return InitMessage(NewMessage(), kTypeNonConfirmable, aUri);
 }
 
 Message *CoapBase::NewPriorityResponseMessage(const Message &aRequest)
@@ -164,18 +144,15 @@
     return InitResponse(NewPriorityMessage(), aRequest);
 }
 
-Message *CoapBase::NewResponseMessage(const Message &aRequest)
-{
-    return InitResponse(NewMessage(), aRequest);
-}
+Message *CoapBase::NewResponseMessage(const Message &aRequest) { return InitResponse(NewMessage(), aRequest); }
 
-Message *CoapBase::InitMessage(Message *aMessage, Type aType, const char *aUriPath)
+Message *CoapBase::InitMessage(Message *aMessage, Type aType, Uri aUri)
 {
     Error error = kErrorNone;
 
     VerifyOrExit(aMessage != nullptr);
 
-    SuccessOrExit(error = aMessage->Init(aType, kCodePost, aUriPath));
+    SuccessOrExit(error = aMessage->Init(aType, kCodePost, aUri));
     SuccessOrExit(error = aMessage->SetPayloadMarker());
 
 exit:
@@ -217,19 +194,19 @@
 }
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-Error CoapBase::SendMessage(Message &                   aMessage,
-                            const Ip6::MessageInfo &    aMessageInfo,
-                            const TxParameters &        aTxParameters,
+Error CoapBase::SendMessage(Message                    &aMessage,
+                            const Ip6::MessageInfo     &aMessageInfo,
+                            const TxParameters         &aTxParameters,
                             ResponseHandler             aHandler,
-                            void *                      aContext,
+                            void                       *aContext,
                             otCoapBlockwiseTransmitHook aTransmitHook,
                             otCoapBlockwiseReceiveHook  aReceiveHook)
 #else
-Error CoapBase::SendMessage(Message &               aMessage,
+Error CoapBase::SendMessage(Message                &aMessage,
                             const Ip6::MessageInfo &aMessageInfo,
-                            const TxParameters &    aTxParameters,
+                            const TxParameters     &aTxParameters,
                             ResponseHandler         aHandler,
-                            void *                  aContext)
+                            void                   *aContext)
 #endif
 {
     Error    error;
@@ -381,10 +358,15 @@
     return error;
 }
 
-Error CoapBase::SendMessage(Message &               aMessage,
+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,
-                            void *                  aContext)
+                            void                   *aContext)
 {
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
     return SendMessage(aMessage, aMessageInfo, TxParameters::GetDefault(), aHandler, aContext, nullptr, nullptr);
@@ -393,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);
@@ -408,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);
@@ -454,7 +446,6 @@
 
     default:
         ExitNow(error = kErrorInvalidArgs);
-        OT_UNREACHABLE_CODE(break);
     }
 
     SuccessOrExit(error = message->SetTokenFromMessage(aRequest));
@@ -521,10 +512,7 @@
             }
         }
 
-        if (nextTime > metadata.mNextTimerShot)
-        {
-            nextTime = metadata.mNextTimerShot;
-        }
+        nextTime = Min(nextTime, metadata.mNextTimerShot);
     }
 
     if (nextTime < now.GetDistantFuture())
@@ -533,9 +521,9 @@
     }
 }
 
-void CoapBase::FinalizeCoapTransaction(Message &               aRequest,
-                                       const Metadata &        aMetadata,
-                                       Message *               aResponse,
+void CoapBase::FinalizeCoapTransaction(Message                &aRequest,
+                                       const Metadata         &aMetadata,
+                                       Message                *aResponse,
                                        const Ip6::MessageInfo *aMessageInfo,
                                        Error                   aResult)
 {
@@ -625,9 +613,9 @@
 
 Error CoapBase::PrepareNextBlockRequest(Message::BlockType aType,
                                         bool               aMoreBlocks,
-                                        Message &          aRequestOld,
-                                        Message &          aRequest,
-                                        Message &          aMessage)
+                                        Message           &aRequestOld,
+                                        Message           &aRequest,
+                                        Message           &aMessage)
 {
     Error            error       = kErrorNone;
     bool             isOptionSet = false;
@@ -685,10 +673,10 @@
     return error;
 }
 
-Error CoapBase::SendNextBlock1Request(Message &               aRequest,
-                                      Message &               aMessage,
+Error CoapBase::SendNextBlock1Request(Message                &aRequest,
+                                      Message                &aMessage,
                                       const Ip6::MessageInfo &aMessageInfo,
-                                      const Metadata &        aCoapMetadata)
+                                      const Metadata         &aCoapMetadata)
 {
     Error    error                = kErrorNone;
     Message *request              = nullptr;
@@ -742,10 +730,10 @@
     return error;
 }
 
-Error CoapBase::SendNextBlock2Request(Message &               aRequest,
-                                      Message &               aMessage,
+Error CoapBase::SendNextBlock2Request(Message                &aRequest,
+                                      Message                &aMessage,
                                       const Ip6::MessageInfo &aMessageInfo,
-                                      const Metadata &        aCoapMetadata,
+                                      const Metadata         &aCoapMetadata,
                                       uint32_t                aTotalLength,
                                       bool                    aBeginBlock1Transfer)
 {
@@ -804,8 +792,8 @@
     return error;
 }
 
-Error CoapBase::ProcessBlock1Request(Message &                aMessage,
-                                     const Ip6::MessageInfo & aMessageInfo,
+Error CoapBase::ProcessBlock1Request(Message                 &aMessage,
+                                     const Ip6::MessageInfo  &aMessageInfo,
                                      const ResourceBlockWise &aResource,
                                      uint32_t                 aTotalLength)
 {
@@ -865,12 +853,12 @@
     return error;
 }
 
-Error CoapBase::ProcessBlock2Request(Message &                aMessage,
-                                     const Ip6::MessageInfo & aMessageInfo,
+Error CoapBase::ProcessBlock2Request(Message                 &aMessage,
+                                     const Ip6::MessageInfo  &aMessageInfo,
                                      const ResourceBlockWise &aResource)
 {
     Error            error                = kErrorNone;
-    Message *        response             = nullptr;
+    Message         *response             = nullptr;
     uint8_t          buf[kMaxBlockLength] = {0};
     uint16_t         bufLen               = kMaxBlockLength;
     bool             moreBlocks           = false;
@@ -893,6 +881,8 @@
     response->Init(kTypeAck, kCodeContent);
     response->SetMessageId(aMessage.GetMessageId());
 
+    SuccessOrExit(error = response->SetTokenFromMessage(aMessage));
+
     VerifyOrExit((bufLen = otCoapBlockSizeFromExponent(aMessage.GetBlockWiseBlockSize())) <= kMaxBlockLength,
                  error = kErrorNoBufs);
     SuccessOrExit(error = aResource.HandleBlockTransmit(buf,
@@ -1011,9 +1001,9 @@
     }
 }
 
-Message *CoapBase::FindRelatedRequest(const Message &         aResponse,
+Message *CoapBase::FindRelatedRequest(const Message          &aResponse,
                                       const Ip6::MessageInfo &aMessageInfo,
-                                      Metadata &              aMetadata)
+                                      Metadata               &aMetadata)
 {
     Message *request = nullptr;
 
@@ -1091,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);
@@ -1191,7 +1181,7 @@
 
                         case kOptionSize2:
                             // ToDo: wait for method to read uint option values
-                            totalTransfereSize = 0;
+                            totalTransferSize = 0;
                             break;
 
                         default:
@@ -1222,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 ||
@@ -1236,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);
@@ -1249,7 +1239,7 @@
             }
 #else  // OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
             {
-                FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, kErrorNone);
+                                  FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, kErrorNone);
             }
 #endif // OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
         }
@@ -1302,17 +1292,17 @@
 {
     char     uriPath[Message::kMaxReceivedUriPath + 1];
     Message *cachedResponse = nullptr;
-    Error    error          = kErrorNotFound;
+    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 != nullptr)
+    if (mInterceptor.IsSet())
     {
-        SuccessOrExit(error = mInterceptor(aMessage, aMessageInfo, mContext));
+        SuccessOrExit(error = mInterceptor.Invoke(aMessage, aMessageInfo));
     }
 
     switch (mResponsesQueue.GetMatchedResponseCopy(aMessage, aMessageInfo, &cachedResponse))
@@ -1320,10 +1310,10 @@
     case kErrorNone:
         cachedResponse->Finish();
         error = Send(*cachedResponse, aMessageInfo);
-
-        OT_FALL_THROUGH;
+        ExitNow();
 
     case kErrorNoBufs:
+        error = kErrorNoBufs;
         ExitNow();
 
     case kErrorNotFound:
@@ -1360,7 +1350,7 @@
 
         case kOptionSize1:
             // ToDo: wait for method to read uint option values
-            totalTransfereSize = 0;
+            totalTransferSize = 0;
             break;
 
         default:
@@ -1386,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);
@@ -1433,6 +1423,12 @@
     SuccessOrExit(error = aMessage.ReadUriPathOptions(uriPath));
 #endif // OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
 
+    if ((mResourceHandler != nullptr) && mResourceHandler(*this, uriPath, aMessage, aMessageInfo))
+    {
+        error = kErrorNone;
+        ExitNow();
+    }
+
     for (const Resource &resource : mResources)
     {
         if (strcmp(resource.mUriPath, uriPath) == 0)
@@ -1443,12 +1439,15 @@
         }
     }
 
-    if (mDefaultHandler)
+    if (mDefaultHandler.IsSet())
     {
-        mDefaultHandler(mDefaultHandlerContext, &aMessage, &aMessageInfo);
+        mDefaultHandler.Invoke(&aMessage, &aMessageInfo);
         error = kErrorNone;
+        ExitNow();
     }
 
+    error = kErrorNotFound;
+
 exit:
 
     if (error != kErrorNone)
@@ -1482,9 +1481,9 @@
 {
 }
 
-Error ResponsesQueue::GetMatchedResponseCopy(const Message &         aRequest,
+Error ResponsesQueue::GetMatchedResponseCopy(const Message          &aRequest,
                                              const Ip6::MessageInfo &aMessageInfo,
-                                             Message **              aResponse)
+                                             Message               **aResponse)
 {
     Error          error = kErrorNone;
     const Message *cacheResponse;
@@ -1523,11 +1522,11 @@
     return response;
 }
 
-void ResponsesQueue::EnqueueResponse(Message &               aMessage,
+void ResponsesQueue::EnqueueResponse(Message                &aMessage,
                                      const Ip6::MessageInfo &aMessageInfo,
-                                     const TxParameters &    aTxParameters)
+                                     const TxParameters     &aTxParameters)
 {
-    Message *        responseCopy;
+    Message         *responseCopy;
     ResponseMetadata metadata;
 
     metadata.mDequeueTime = TimerMilli::GetNow() + aTxParameters.CalculateExchangeLifetime();
@@ -1552,7 +1551,7 @@
 void ResponsesQueue::UpdateQueue(void)
 {
     uint16_t  msgCount    = 0;
-    Message * earliestMsg = nullptr;
+    Message  *earliestMsg = nullptr;
     TimeMilli earliestDequeueTime(0);
 
     // Check the number of messages in the queue and if number is at
@@ -1580,15 +1579,9 @@
     }
 }
 
-void ResponsesQueue::DequeueResponse(Message &aMessage)
-{
-    mQueue.DequeueAndFree(aMessage);
-}
+void ResponsesQueue::DequeueResponse(Message &aMessage) { mQueue.DequeueAndFree(aMessage); }
 
-void ResponsesQueue::DequeueAllResponses(void)
-{
-    mQueue.DequeueAndFreeAll();
-}
+void ResponsesQueue::DequeueAllResponses(void) { mQueue.DequeueAndFreeAll(); }
 
 void ResponsesQueue::HandleTimer(Timer &aTimer)
 {
@@ -1612,10 +1605,7 @@
             continue;
         }
 
-        if (metadata.mDequeueTime < nextDequeueTime)
-        {
-            nextDequeueTime = metadata.mDequeueTime;
-        }
+        nextDequeueTime = Min(nextDequeueTime, metadata.mDequeueTime);
     }
 
     if (nextDequeueTime < now.GetDistantFuture())
@@ -1653,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);
@@ -1677,10 +1667,7 @@
     return CalculateSpan(mMaxRetransmit) + 2 * kDefaultMaxLatency + mAckTimeout;
 }
 
-uint32_t TxParameters::CalculateMaxTransmitWait(void) const
-{
-    return CalculateSpan(mMaxRetransmit + 1);
-}
+uint32_t TxParameters::CalculateMaxTransmitWait(void) const { return CalculateSpan(mMaxRetransmit + 1); }
 
 uint32_t TxParameters::CalculateSpan(uint8_t aMaxRetx) const
 {
@@ -1695,13 +1682,30 @@
     kDefaultMaxRetransmit,
 };
 
+//----------------------------------------------------------------------------------------------------------------------
+
+Resource::Resource(const char *aUriPath, RequestHandler aHandler, void *aContext)
+{
+    mUriPath = aUriPath;
+    mHandler = aHandler;
+    mContext = aContext;
+    mNext    = nullptr;
+}
+
+Resource::Resource(Uri aUri, RequestHandler aHandler, void *aContext)
+    : Resource(PathForUri(aUri), aHandler, aContext)
+{
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+
 Coap::Coap(Instance &aInstance)
     : CoapBase(aInstance, &Coap::Send)
     , mSocket(aInstance)
 {
 }
 
-Error Coap::Start(uint16_t aPort, otNetifIdentifier aNetifIdentifier)
+Error Coap::Start(uint16_t aPort, Ip6::NetifIdentifier aNetifIdentifier)
 {
     Error error        = kErrorNone;
     bool  socketOpened = false;
diff --git a/src/core/coap/coap.hpp b/src/core/coap/coap.hpp
index aea240e..b5d05a6 100644
--- a/src/core/coap/coap.hpp
+++ b/src/core/coap/coap.hpp
@@ -35,6 +35,7 @@
 
 #include "coap/coap_message.hpp"
 #include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/debug.hpp"
 #include "common/linked_list.hpp"
 #include "common/locator.hpp"
@@ -44,6 +45,7 @@
 #include "net/ip6.hpp"
 #include "net/netif.hpp"
 #include "net/udp6.hpp"
+#include "thread/uri_paths.hpp"
 
 /**
  * @file
@@ -150,14 +152,19 @@
      * @param[in]  aUriPath  A pointer to a null-terminated string for the URI path.
      * @param[in]  aHandler  A function pointer that is called when receiving a CoAP message for @p aUriPath.
      * @param[in]  aContext  A pointer to arbitrary context information.
+     *
      */
-    Resource(const char *aUriPath, RequestHandler aHandler, void *aContext)
-    {
-        mUriPath = aUriPath;
-        mHandler = aHandler;
-        mContext = aContext;
-        mNext    = nullptr;
-    }
+    Resource(const char *aUriPath, RequestHandler aHandler, void *aContext);
+
+    /**
+     * This constructor initializes the resource.
+     *
+     * @param[in]  aUri      A Thread URI.
+     * @param[in]  aHandler  A function pointer that is called when receiving a CoAP message for the URI.
+     * @param[in]  aContext  A pointer to arbitrary context information.
+     *
+     */
+    Resource(Uri aUri, RequestHandler aHandler, void *aContext);
 
     /**
      * This method returns a pointer to the URI path.
@@ -195,9 +202,9 @@
      * @param[in]  aTransmitHook    A function pointer that is called when transmitting a CoAP block message from @p
      *                              aUriPath.
      */
-    ResourceBlockWise(const char *                aUriPath,
+    ResourceBlockWise(const char                 *aUriPath,
                       otCoapRequestHandler        aHandler,
-                      void *                      aContext,
+                      void                       *aContext,
                       otCoapBlockwiseReceiveHook  aReceiveHook,
                       otCoapBlockwiseTransmitHook aTransmitHook)
     {
@@ -432,7 +439,7 @@
      * @param[in]  aContext   A pointer to arbitrary context information. May be `nullptr` if not used.
      *
      */
-    void SetDefaultHandler(RequestHandler aHandler, void *aContext);
+    void SetDefaultHandler(RequestHandler aHandler, void *aContext) { mDefaultHandler.Set(aHandler, aContext); }
 
     /**
      * This method allocates a new message with a CoAP header.
@@ -442,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.
@@ -450,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.
@@ -463,58 +475,58 @@
      * Even if message has no payload, calling `SetPayloadMarker()` is harmless, since `SendMessage()` will check and
      * remove the payload marker when there is no payload.
      *
-     * @param[in] aUriPath   The URI path string.
+     * @param[in] aUri      The URI.
      *
      * @returns A pointer to the message or `nullptr` if failed to allocate message.
      *
      */
-    Message *NewPriorityConfirmablePostMessage(const char *aUriPath);
+    Message *NewPriorityConfirmablePostMessage(Uri aUri);
 
     /**
      * This method allocates and initializes a new CoAP Confirmable Post message with normal priority level.
      *
-     * The CoAP header is initialized as `kTypeConfirmable` and `kCodePost` with a given URI path and a randomly
+     * The CoAP header is initialized as `kTypeConfirmable` and `kCodePost` with a given URI and a randomly
      * generated token (of default length). This method also sets the payload marker (calling `SetPayloadMarker()`).
      * Even if message has no payload, calling `SetPayloadMarker()` is harmless, since `SendMessage()` will check and
      * remove the payload marker when there is no payload.
      *
-     * @param[in] aUriPath   The URI path string.
+     * @param[in] aUri      The URI.
      *
      * @returns A pointer to the message or `nullptr` if failed to allocate message.
      *
      */
-    Message *NewConfirmablePostMessage(const char *aUriPath);
+    Message *NewConfirmablePostMessage(Uri aUri);
 
     /**
      * This method allocates and initializes a new CoAP Non-confirmable Post message with Network Control priority
      * level.
      *
-     * The CoAP header is initialized as `kTypeNonConfirmable` and `kCodePost` with a given URI path and a randomly
+     * The CoAP header is initialized as `kTypeNonConfirmable` and `kCodePost` with a given URI and a randomly
      * generated token (of default length). This method also sets the payload marker (calling `SetPayloadMarker()`).
      * Even if message has no payload, calling `SetPayloadMarker()` is harmless, since `SendMessage()` will check and
      * remove the payload marker when there is no payload.
      *
-     * @param[in] aUriPath   The URI path string.
+     * @param[in] aUri      The URI.
      *
      * @returns A pointer to the message or `nullptr` if failed to allocate message.
      *
      */
-    Message *NewPriorityNonConfirmablePostMessage(const char *aUriPath);
+    Message *NewPriorityNonConfirmablePostMessage(Uri aUri);
 
     /**
      * This method allocates and initializes a new CoAP Non-confirmable Post message with normal priority level.
      *
-     * The CoAP header is initialized as `kTypeNonConfirmable` and `kCodePost` with a given URI path and a randomly
+     * The CoAP header is initialized as `kTypeNonConfirmable` and `kCodePost` with a given URI and a randomly
      * generated token (of default length). This method also sets the payload marker (calling `SetPayloadMarker()`).
      * Even if message has no payload, calling `SetPayloadMarker()` is harmless, since `SendMessage()` will check and
      * remove the payload marker when there is no payload.
      *
-     * @param[in] aUriPath   The URI path string.
+     * @param[in] aUri      The URI.
      *
      * @returns A pointer to the message or `nullptr` if failed to allocate message.
      *
      */
-    Message *NewNonConfirmablePostMessage(const char *aUriPath);
+    Message *NewNonConfirmablePostMessage(Uri aUri);
 
     /**
      * This method allocates and initializes a new CoAP response message with Network Control priority level for a
@@ -550,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.
@@ -564,11 +576,11 @@
      * @retval kErrorNoBufs   Failed to allocate retransmission data.
      *
      */
-    Error SendMessage(Message &                   aMessage,
-                      const Ip6::MessageInfo &    aMessageInfo,
-                      const TxParameters &        aTxParameters,
+    Error SendMessage(Message                    &aMessage,
+                      const Ip6::MessageInfo     &aMessageInfo,
+                      const TxParameters         &aTxParameters,
                       otCoapResponseHandler       aHandler      = nullptr,
-                      void *                      aContext      = nullptr,
+                      void                       *aContext      = nullptr,
                       otCoapBlockwiseTransmitHook aTransmitHook = nullptr,
                       otCoapBlockwiseReceiveHook  aReceiveHook  = nullptr);
 #else  // OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
@@ -578,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.
@@ -590,19 +602,31 @@
      * @retval kErrorNoBufs  Insufficient buffers available to send the CoAP message.
      *
      */
-    Error SendMessage(Message &               aMessage,
+    Error SendMessage(Message                &aMessage,
                       const Ip6::MessageInfo &aMessageInfo,
-                      const TxParameters &    aTxParameters,
-                      ResponseHandler         aHandler = nullptr,
-                      void *                  aContext = nullptr);
+                      const TxParameters     &aTxParameters,
+                      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.
@@ -613,10 +637,24 @@
      * @retval kErrorNoBufs  Insufficient buffers available to send the CoAP response.
      *
      */
-    Error SendMessage(Message &               aMessage,
+    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.
@@ -670,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.
@@ -723,7 +774,7 @@
      * @param[in]   aContext        A pointer to arbitrary context information.
      *
      */
-    void SetInterceptor(Interceptor aInterceptor, void *aContext);
+    void SetInterceptor(Interceptor aInterceptor, void *aContext) { mInterceptor.Set(aInterceptor, aContext); }
 
     /**
      * This method returns a reference to the request message list.
@@ -743,6 +794,26 @@
 
 protected:
     /**
+     * This type defines function pointer to handle a CoAP resource.
+     *
+     * When processing a received request, this handler is called first with the URI path before checking the list of
+     * added `Resource` entries to match against the URI path.
+     *
+     * @param[in] aCoapBase     A reference the CoAP agent.
+     * @param[in] aUriPath      The URI Path string.
+     * @param[in] aMessage      The received message.
+     * @param[in] aMessageInfo  The message info associated with @p aMessage.
+     *
+     * @retval TRUE   Indicates that the URI path was known and the message was processed by the handler.
+     * @retval FALSE  Indicates that URI path was not known and the message was not processed by the handler.
+     *
+     */
+    typedef bool (*ResourceHandler)(CoapBase               &aCoapBase,
+                                    const char             *aUriPath,
+                                    Message                &aMessage,
+                                    const Ip6::MessageInfo &aMessageInfo);
+
+    /**
      * This function pointer is called to send a CoAP message.
      *
      * @param[in]  aCoapBase     A reference to the CoAP agent.
@@ -774,6 +845,14 @@
      */
     void Receive(ot::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
+    /**
+     * This method sets the resource handler function.
+     *
+     * @param[in] aResourceHandler   The resource handler function pointer.
+     *
+     */
+    void SetResourceHandler(ResourceHandler aHandler) { mResourceHandler = aHandler; }
+
 private:
     struct Metadata
     {
@@ -785,7 +864,7 @@
         Ip6::Address    mDestinationAddress;       // IPv6 address of the message destination.
         uint16_t        mDestinationPort;          // UDP port of the message destination.
         ResponseHandler mResponseHandler;          // A function pointer that is called on response reception.
-        void *          mResponseContext;          // A pointer to arbitrary context information.
+        void           *mResponseContext;          // A pointer to arbitrary context information.
         TimeMilli       mNextTimerShot;            // Time when the timer should shoot for this message.
         uint32_t        mRetransmissionTimeout;    // Delay that is applied to next retransmission.
         uint8_t         mRetransmissionsRemaining; // Number of retransmissions remaining.
@@ -807,7 +886,7 @@
 #endif
     };
 
-    Message *InitMessage(Message *aMessage, Type aType, const char *aUriPath);
+    Message *InitMessage(Message *aMessage, Type aType, Uri aUri);
     Message *InitResponse(Message *aMessage, const Message &aResponse);
 
     static void HandleRetransmissionTimer(Timer &aTimer);
@@ -817,9 +896,9 @@
     Message *CopyAndEnqueueMessage(const Message &aMessage, uint16_t aCopyLength, const Metadata &aMetadata);
     void     DequeueMessage(Message &aMessage);
     Message *FindRelatedRequest(const Message &aResponse, const Ip6::MessageInfo &aMessageInfo, Metadata &aMetadata);
-    void     FinalizeCoapTransaction(Message &               aRequest,
-                                     const Metadata &        aMetadata,
-                                     Message *               aResponse,
+    void     FinalizeCoapTransaction(Message                &aRequest,
+                                     const Metadata         &aMetadata,
+                                     Message                *aResponse,
                                      const Ip6::MessageInfo *aMessageInfo,
                                      Error                   aResult);
 
@@ -829,29 +908,29 @@
 
     Error PrepareNextBlockRequest(Message::BlockType aType,
                                   bool               aMoreBlocks,
-                                  Message &          aRequestOld,
-                                  Message &          aRequest,
-                                  Message &          aMessage);
-    Error ProcessBlock1Request(Message &                aMessage,
-                               const Ip6::MessageInfo & aMessageInfo,
+                                  Message           &aRequestOld,
+                                  Message           &aRequest,
+                                  Message           &aMessage);
+    Error ProcessBlock1Request(Message                 &aMessage,
+                               const Ip6::MessageInfo  &aMessageInfo,
                                const ResourceBlockWise &aResource,
                                uint32_t                 aTotalLength);
-    Error ProcessBlock2Request(Message &                aMessage,
-                               const Ip6::MessageInfo & aMessageInfo,
+    Error ProcessBlock2Request(Message                 &aMessage,
+                               const Ip6::MessageInfo  &aMessageInfo,
                                const ResourceBlockWise &aResource);
 #endif
     void ProcessReceivedRequest(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
     void ProcessReceivedResponse(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-    Error SendNextBlock1Request(Message &               aRequest,
-                                Message &               aMessage,
+    Error SendNextBlock1Request(Message                &aRequest,
+                                Message                &aMessage,
                                 const Ip6::MessageInfo &aMessageInfo,
-                                const Metadata &        aCoapMetadata);
-    Error SendNextBlock2Request(Message &               aRequest,
-                                Message &               aMessage,
+                                const Metadata         &aCoapMetadata);
+    Error SendNextBlock2Request(Message                &aRequest,
+                                Message                &aMessage,
                                 const Ip6::MessageInfo &aMessageInfo,
-                                const Metadata &        aCoapMetadata,
+                                const Metadata         &aCoapMetadata,
                                 uint32_t                aTotalLength,
                                 bool                    aBeginBlock1Transfer);
 #endif
@@ -866,18 +945,18 @@
 
     LinkedList<Resource> mResources;
 
-    void *         mContext;
-    Interceptor    mInterceptor;
-    ResponsesQueue mResponsesQueue;
+    Callback<Interceptor> mInterceptor;
+    ResponsesQueue        mResponsesQueue;
 
-    RequestHandler mDefaultHandler;
-    void *         mDefaultHandlerContext;
+    Callback<RequestHandler> mDefaultHandler;
+
+    ResourceHandler mResourceHandler;
 
     const Sender mSender;
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
     LinkedList<ResourceBlockWise> mBlockWiseResources;
-    Message *                     mLastResponse;
+    Message                      *mLastResponse;
 #endif
 };
 
@@ -906,7 +985,7 @@
      * @retval kErrorFailed  Failed to start CoAP agent.
      *
      */
-    Error Start(uint16_t aPort, otNetifIdentifier aNetifIdentifier = OT_NETIF_UNSPECIFIED);
+    Error Start(uint16_t aPort, Ip6::NetifIdentifier aNetifIdentifier = Ip6::kNetifUnspecified);
 
     /**
      * This method stops the CoAP service.
diff --git a/src/core/coap/coap_message.cpp b/src/core/coap/coap_message.cpp
index bd1c152..e4ef990 100644
--- a/src/core/coap/coap_message.cpp
+++ b/src/core/coap/coap_message.cpp
@@ -67,32 +67,26 @@
     SetCode(aCode);
 }
 
-Error Message::Init(Type aType, Code aCode, const char *aUriPath)
+Error Message::Init(Type aType, Code aCode, Uri aUri)
 {
     Error error;
 
     Init(aType, aCode);
     SuccessOrExit(error = GenerateRandomToken(kDefaultTokenLength));
-    SuccessOrExit(error = AppendUriPathOptions(aUriPath));
+    SuccessOrExit(error = AppendUriPathOptions(PathForUri(aUri)));
 
 exit:
     return error;
 }
 
-Error Message::InitAsPost(const Ip6::Address &aDestination, const char *aUriPath)
+Error Message::InitAsPost(const Ip6::Address &aDestination, Uri aUri)
 {
-    return Init(aDestination.IsMulticast() ? kTypeNonConfirmable : kTypeConfirmable, kCodePost, aUriPath);
+    return Init(aDestination.IsMulticast() ? kTypeNonConfirmable : kTypeConfirmable, kCodePost, aUri);
 }
 
-bool Message::IsConfirmablePostRequest(void) const
-{
-    return IsConfirmable() && IsPostRequest();
-}
+bool Message::IsConfirmablePostRequest(void) const { return IsConfirmable() && IsPostRequest(); }
 
-bool Message::IsNonConfirmablePostRequest(void) const
-{
-    return IsNonConfirmable() && IsPostRequest();
-}
+bool Message::IsNonConfirmablePostRequest(void) const { return IsNonConfirmable() && IsPostRequest(); }
 
 void Message::Finish(void)
 {
@@ -225,7 +219,7 @@
 
 Error Message::ReadUriPathOptions(char (&aUriPath)[kMaxReceivedUriPath + 1]) const
 {
-    char *           curUriPath = aUriPath;
+    char            *curUriPath = aUriPath;
     Error            error      = kErrorNone;
     Option::Iterator iterator;
 
@@ -456,15 +450,9 @@
 }
 #endif // OPENTHREAD_CONFIG_COAP_API_ENABLE
 
-Message::Iterator MessageQueue::begin(void)
-{
-    return Message::Iterator(GetHead());
-}
+Message::Iterator MessageQueue::begin(void) { return Message::Iterator(GetHead()); }
 
-Message::ConstIterator MessageQueue::begin(void) const
-{
-    return Message::ConstIterator(GetHead());
-}
+Message::ConstIterator MessageQueue::begin(void) const { return Message::ConstIterator(GetHead()); }
 
 Error Option::Iterator::Init(const Message &aMessage)
 {
diff --git a/src/core/coap/coap_message.hpp b/src/core/coap/coap_message.hpp
index 1f4a86b..40c9c97 100644
--- a/src/core/coap/coap_message.hpp
+++ b/src/core/coap/coap_message.hpp
@@ -47,6 +47,7 @@
 #include "net/ip6.hpp"
 #include "net/ip6_address.hpp"
 #include "net/udp6.hpp"
+#include "thread/uri_paths.hpp"
 
 namespace ot {
 
@@ -208,13 +209,13 @@
      *
      * @param[in]  aType              The Type value.
      * @param[in]  aCode              The Code value.
-     * @param[in]  aUriPath           A pointer to a null-terminated string.
+     * @param[in]  aUri               The URI.
      *
      * @retval kErrorNone         Successfully appended the option.
      * @retval kErrorNoBufs       The option length exceeds the buffer size.
      *
      */
-    Error Init(Type aType, Code aCode, const char *aUriPath);
+    Error Init(Type aType, Code aCode, Uri aUri);
 
     /**
      * This method initializes the CoAP header as `kCodePost` with a given URI Path with its type determined from a
@@ -222,13 +223,13 @@
      *
      * @param[in]  aDestination       The message destination IPv6 address used to determine the CoAP type,
      *                                `kTypeNonConfirmable` if multicast address, `kTypeConfirmable` otherwise.
-     * @param[in]  aUriPath           A pointer to a null-terminated string.
+     * @param[in]  aUri               The URI.
      *
      * @retval kErrorNone         Successfully appended the option.
      * @retval kErrorNoBufs       The option length exceeds the buffer size.
      *
      */
-    Error InitAsPost(const Ip6::Address &aDestination, const char *aUriPath);
+    Error InitAsPost(const Ip6::Address &aDestination, Uri aUri);
 
     /**
      * This method writes header to the message. This must be called before sending the message.
@@ -633,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.
      *
@@ -944,14 +945,14 @@
         Message *operator->(void) { return static_cast<Message *>(ot::Message::Iterator::operator->()); }
     };
 
-    static_assert(sizeof(HelpData) <= sizeof(Ip6::Header) + sizeof(Ip6::HopByHopHeader) + sizeof(Ip6::OptionMpl) +
+    static_assert(sizeof(HelpData) <= sizeof(Ip6::Header) + sizeof(Ip6::HopByHopHeader) + sizeof(Ip6::MplOption) +
                                           sizeof(Ip6::Udp::Header),
                   "HelpData size exceeds the size of the reserved region in the message");
 
     const HelpData &GetHelpData(void) const
     {
         static_assert(sizeof(HelpData) + kHelpDataAlignment <= kHeadBufferDataSize,
-                      "Insufficient buffer size for CoAP processing!");
+                      "Insufficient buffer size for CoAP processing! Increase OPENTHREAD_CONFIG_MESSAGE_BUFFER_SIZE.");
 
         return *static_cast<const HelpData *>(OT_ALIGN(GetFirstData(), kHelpDataAlignment));
     }
@@ -1240,10 +1241,7 @@
  * @returns A reference to `Coap::Message` matching @p aMessage.
  *
  */
-inline Coap::Message &AsCoapMessage(otMessage *aMessage)
-{
-    return *static_cast<Coap::Message *>(aMessage);
-}
+inline Coap::Message &AsCoapMessage(otMessage *aMessage) { return *static_cast<Coap::Message *>(aMessage); }
 
 /**
  * This method casts an `otMessage` pointer to a `Coap::Message` reference.
@@ -1253,10 +1251,7 @@
  * @returns A reference to `Coap::Message` matching @p aMessage.
  *
  */
-inline Coap::Message *AsCoapMessagePtr(otMessage *aMessage)
-{
-    return static_cast<Coap::Message *>(aMessage);
-}
+inline Coap::Message *AsCoapMessagePtr(otMessage *aMessage) { return static_cast<Coap::Message *>(aMessage); }
 
 /**
  * This method casts an `otMessage` pointer to a `Coap::Message` pointer.
diff --git a/src/core/coap/coap_secure.cpp b/src/core/coap/coap_secure.cpp
index d158731..3f5730a 100644
--- a/src/core/coap/coap_secure.cpp
+++ b/src/core/coap/coap_secure.cpp
@@ -50,8 +50,6 @@
 CoapSecure::CoapSecure(Instance &aInstance, bool aLayerTwoSecurity)
     : CoapBase(aInstance, &CoapSecure::Send)
     , mDtls(aInstance, aLayerTwoSecurity)
-    , mConnectedCallback(nullptr)
-    , mConnectedContext(nullptr)
     , mTransmitTask(aInstance, CoapSecure::HandleTransmit, this)
 {
 }
@@ -60,8 +58,7 @@
 {
     Error error = kErrorNone;
 
-    mConnectedCallback = nullptr;
-    mConnectedContext  = nullptr;
+    mConnectedCallback.Clear();
 
     SuccessOrExit(error = mDtls.Open(&CoapSecure::HandleDtlsReceive, &CoapSecure::HandleDtlsConnected, this));
     SuccessOrExit(error = mDtls.Bind(aPort));
@@ -74,8 +71,7 @@
 {
     Error error = kErrorNone;
 
-    mConnectedCallback = nullptr;
-    mConnectedContext  = nullptr;
+    mConnectedCallback.Clear();
 
     SuccessOrExit(error = mDtls.Open(&CoapSecure::HandleDtlsReceive, &CoapSecure::HandleDtlsConnected, this));
     SuccessOrExit(error = mDtls.Bind(aCallback, aContext));
@@ -94,8 +90,7 @@
 
 Error CoapSecure::Connect(const Ip6::SockAddr &aSockAddr, ConnectedCallback aCallback, void *aContext)
 {
-    mConnectedCallback = aCallback;
-    mConnectedContext  = aContext;
+    mConnectedCallback.Set(aCallback, aContext);
 
     return mDtls.Connect(aSockAddr);
 }
@@ -110,9 +105,9 @@
 }
 
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-Error CoapSecure::SendMessage(Message &                   aMessage,
+Error CoapSecure::SendMessage(Message                    &aMessage,
                               ResponseHandler             aHandler,
-                              void *                      aContext,
+                              void                       *aContext,
                               otCoapBlockwiseTransmitHook aTransmitHook,
                               otCoapBlockwiseReceiveHook  aReceiveHook)
 {
@@ -127,10 +122,10 @@
     return error;
 }
 
-Error CoapSecure::SendMessage(Message &                   aMessage,
-                              const Ip6::MessageInfo &    aMessageInfo,
+Error CoapSecure::SendMessage(Message                    &aMessage,
+                              const Ip6::MessageInfo     &aMessageInfo,
                               ResponseHandler             aHandler,
-                              void *                      aContext,
+                              void                       *aContext,
                               otCoapBlockwiseTransmitHook aTransmitHook,
                               otCoapBlockwiseReceiveHook  aReceiveHook)
 {
@@ -150,10 +145,10 @@
     return error;
 }
 
-Error CoapSecure::SendMessage(Message &               aMessage,
+Error CoapSecure::SendMessage(Message                &aMessage,
                               const Ip6::MessageInfo &aMessageInfo,
                               ResponseHandler         aHandler,
-                              void *                  aContext)
+                              void                   *aContext)
 {
     return CoapBase::SendMessage(aMessage, aMessageInfo, aHandler, aContext);
 }
@@ -174,13 +169,7 @@
     return static_cast<CoapSecure *>(aContext)->HandleDtlsConnected(aConnected);
 }
 
-void CoapSecure::HandleDtlsConnected(bool aConnected)
-{
-    if (mConnectedCallback != nullptr)
-    {
-        mConnectedCallback(aConnected, mConnectedContext);
-    }
-}
+void CoapSecure::HandleDtlsConnected(bool aConnected) { mConnectedCallback.InvokeIfSet(aConnected); }
 
 void CoapSecure::HandleDtlsReceive(void *aContext, uint8_t *aBuf, uint16_t aLength)
 {
diff --git a/src/core/coap/coap_secure.hpp b/src/core/coap/coap_secure.hpp
index be8caf6..43b2d02 100644
--- a/src/core/coap/coap_secure.hpp
+++ b/src/core/coap/coap_secure.hpp
@@ -34,6 +34,7 @@
 #if OPENTHREAD_CONFIG_DTLS_ENABLE
 
 #include "coap/coap.hpp"
+#include "common/callback.hpp"
 #include "meshcop/dtls.hpp"
 #include "meshcop/meshcop.hpp"
 
@@ -101,8 +102,7 @@
      */
     void SetConnectedCallback(ConnectedCallback aCallback, void *aContext)
     {
-        mConnectedCallback = aCallback;
-        mConnectedContext  = aContext;
+        mConnectedCallback.Set(aCallback, aContext);
     }
 
     /**
@@ -267,8 +267,7 @@
      */
     void SetClientConnectedCallback(ConnectedCallback aCallback, void *aContext)
     {
-        mConnectedCallback = aCallback;
-        mConnectedContext  = aContext;
+        mConnectedCallback.Set(aCallback, aContext);
     }
 
     /**
@@ -301,9 +300,9 @@
      * @retval kErrorInvalidState  DTLS connection was not initialized.
      *
      */
-    Error SendMessage(Message &                   aMessage,
+    Error SendMessage(Message                    &aMessage,
                       ResponseHandler             aHandler      = nullptr,
-                      void *                      aContext      = nullptr,
+                      void                       *aContext      = nullptr,
                       otCoapBlockwiseTransmitHook aTransmitHook = nullptr,
                       otCoapBlockwiseReceiveHook  aReceiveHook  = nullptr);
 
@@ -326,10 +325,10 @@
      * @retval kErrorInvalidState  DTLS connection was not initialized.
      *
      */
-    Error SendMessage(Message &                   aMessage,
-                      const Ip6::MessageInfo &    aMessageInfo,
+    Error SendMessage(Message                    &aMessage,
+                      const Ip6::MessageInfo     &aMessageInfo,
                       ResponseHandler             aHandler      = nullptr,
-                      void *                      aContext      = nullptr,
+                      void                       *aContext      = nullptr,
                       otCoapBlockwiseTransmitHook aTransmitHook = nullptr,
                       otCoapBlockwiseReceiveHook  aReceiveHook  = nullptr);
 #else  // OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
@@ -368,10 +367,10 @@
      * @retval kErrorInvalidState  DTLS connection was not initialized.
      *
      */
-    Error SendMessage(Message &               aMessage,
+    Error SendMessage(Message                &aMessage,
                       const Ip6::MessageInfo &aMessageInfo,
                       ResponseHandler         aHandler = nullptr,
-                      void *                  aContext = nullptr);
+                      void                   *aContext = nullptr);
 #endif // OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
 
     /**
@@ -410,11 +409,10 @@
     static void HandleTransmit(Tasklet &aTasklet);
     void        HandleTransmit(void);
 
-    MeshCoP::Dtls     mDtls;
-    ConnectedCallback mConnectedCallback;
-    void *            mConnectedContext;
-    ot::MessageQueue  mTransmitQueue;
-    TaskletContext    mTransmitTask;
+    MeshCoP::Dtls               mDtls;
+    Callback<ConnectedCallback> mConnectedCallback;
+    ot::MessageQueue            mTransmitQueue;
+    TaskletContext              mTransmitTask;
 };
 
 } // namespace Coap
diff --git a/src/core/common/appender.cpp b/src/core/common/appender.cpp
index caa46e0..1504294 100644
--- a/src/core/common/appender.cpp
+++ b/src/core/common/appender.cpp
@@ -84,7 +84,7 @@
     return length;
 }
 
-void Appender::GetAsData(Data<kWithUint16Length> &aData)
+void Appender::GetAsData(Data<kWithUint16Length> &aData) const
 {
     aData.Init(mShared.mFrameBuilder.GetBytes(), mShared.mFrameBuilder.GetLength());
 }
diff --git a/src/core/common/appender.hpp b/src/core/common/appender.hpp
index 9154c53..5260ba8 100644
--- a/src/core/common/appender.hpp
+++ b/src/core/common/appender.hpp
@@ -142,7 +142,7 @@
      * @returns The `Message` instance associated with `Appender`.
      *
      */
-    Message &GetMessage(void) { return *mShared.mMessage.mMessage; }
+    Message &GetMessage(void) const { return *mShared.mMessage.mMessage; }
 
     /**
      * This method returns a pointer to the start of the data buffer associated with `Appender`.
@@ -152,7 +152,7 @@
      * @returns A pointer to the start of the data buffer associated with `Appender`.
      *
      */
-    uint8_t *GetBufferStart(void) { return AsNonConst(mShared.mFrameBuilder.GetBytes()); }
+    uint8_t *GetBufferStart(void) const { return AsNonConst(mShared.mFrameBuilder.GetBytes()); }
 
     /**
      * This method gets the data buffer associated with `Appender` as a `Data`.
@@ -162,7 +162,7 @@
      * @pram[out] aData  A reference to a `Data` to output the data buffer.
      *
      */
-    void GetAsData(Data<kWithUint16Length> &aData);
+    void GetAsData(Data<kWithUint16Length> &aData) const;
 
 private:
     Type mType;
diff --git a/src/core/common/array.hpp b/src/core/common/array.hpp
index 2afb4fd..4a65073 100644
--- a/src/core/common/array.hpp
+++ b/src/core/common/array.hpp
@@ -39,6 +39,7 @@
 #include "common/code_utils.hpp"
 #include "common/const_cast.hpp"
 #include "common/error.hpp"
+#include "common/locator.hpp"
 #include "common/numeric_limits.hpp"
 #include "common/type_traits.hpp"
 
@@ -147,6 +148,24 @@
     Array(const Array &aOtherArray) { *this = aOtherArray; }
 
     /**
+     * This constructor initializes the array as empty and initializes its elements by calling `Init(Instance &)`
+     * method on every element.
+     *
+     * This constructor uses method `Init(Instance &aInstance)` on `Type`.
+     *
+     * @param[in] aInstance  The OpenThread instance.
+     *
+     */
+    explicit Array(Instance &aInstance)
+        : mLength(0)
+    {
+        for (Type &element : mElements)
+        {
+            element.Init(aInstance);
+        }
+    }
+
+    /**
      * This method clears the array.
      *
      */
@@ -187,6 +206,30 @@
     IndexType GetLength(void) const { return mLength; }
 
     /**
+     * This methods sets the current length (number of elements) of the array.
+     *
+     * @param[in] aLength   The array length.
+     *
+     */
+    void SetLength(IndexType aLength) { mLength = aLength; }
+
+    /**
+     * This method returns the pointer to the start of underlying C array buffer serving as `Array` storage.
+     *
+     * @return The pointer to start of underlying C array buffer.
+     *
+     */
+    Type *GetArrayBuffer(void) { return mElements; }
+
+    /**
+     * This method returns the pointer to the start of underlying C array buffer serving as `Array` storage.
+     *
+     * @return The pointer to start of underlying C array buffer.
+     *
+     */
+    const Type *GetArrayBuffer(void) const { return mElements; }
+
+    /**
      * This method overloads the `[]` operator to get the element at a given index.
      *
      * This method does not perform index bounds checking. Behavior is undefined if @p aIndex is not valid.
@@ -534,12 +577,29 @@
         return *this;
     }
 
+    /**
+     * This method indicates whether a given entry pointer is from the array buffer.
+     *
+     * This method does not check the current length of array and only checks that @p aEntry is pointing to an address
+     * contained within underlying C array buffer.
+     *
+     * @param[in] aEntry   A pointer to an entry to check.
+     *
+     * @retval TRUE  The @p aEntry is from the array.
+     * @retval FALSE The @p aEntry is not from the array.
+     *
+     */
+    bool IsInArrayBuffer(const Type *aEntry) const
+    {
+        return (&mElements[0] <= aEntry) && (aEntry < GetArrayEnd(mElements));
+    }
+
     // The following methods are intended to support range-based `for`
     // loop iteration over the array elements and should not be used
     // directly.
 
-    Type *      begin(void) { return &mElements[0]; }
-    Type *      end(void) { return &mElements[mLength]; }
+    Type       *begin(void) { return &mElements[0]; }
+    Type       *end(void) { return &mElements[mLength]; }
     const Type *begin(void) const { return &mElements[0]; }
     const Type *end(void) const { return &mElements[mLength]; }
 
diff --git a/src/core/common/as_core_type.hpp b/src/core/common/as_core_type.hpp
index e5ad9bc..521cabb 100644
--- a/src/core/common/as_core_type.hpp
+++ b/src/core/common/as_core_type.hpp
@@ -36,6 +36,8 @@
 
 #include "openthread-core-config.h"
 
+#include "common/debug.hpp"
+
 namespace ot {
 
 /**
@@ -63,6 +65,8 @@
  */
 template <typename Type> typename CoreType<Type>::Type &AsCoreType(Type *aObject)
 {
+    AssertPointerIsNotNull(aObject);
+
     return *static_cast<typename CoreType<Type>::Type *>(aObject);
 }
 
@@ -78,6 +82,8 @@
  */
 template <typename Type> const typename CoreType<Type>::Type &AsCoreType(const Type *aObject)
 {
+    AssertPointerIsNotNull(aObject);
+
     return *static_cast<const typename CoreType<Type>::Type *>(aObject);
 }
 
diff --git a/src/core/common/callback.hpp b/src/core/common/callback.hpp
new file mode 100644
index 0000000..02e4cf3
--- /dev/null
+++ b/src/core/common/callback.hpp
@@ -0,0 +1,243 @@
+/*
+ *  Copyright (c) 2022, 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 defines OpenThread `Callback` class.
+ */
+
+#ifndef CALLBACK_HPP_
+#define CALLBACK_HPP_
+
+#include "openthread-core-config.h"
+
+#include <stdint.h>
+
+#include "common/type_traits.hpp"
+
+namespace ot {
+
+/**
+ * This enumeration specifies the context argument position in a callback function pointer.
+ *
+ */
+enum CallbackContextPosition : uint8_t
+{
+    kContextAsFirstArg, ///< Context is the first argument.
+    kContextAsLastArg,  ///< Context is the last argument.
+};
+
+/**
+ * This class is the base class for `Callback` (a function pointer handler and a `void *` context).
+ *
+ * @tparam HandlerType    The handler function pointer type.
+ *
+ */
+template <typename HandlerType> class CallbackBase
+{
+public:
+    /**
+     * This method clears the `Callback` by setting the handler function pointer to `nullptr`.
+     *
+     */
+    void Clear(void) { mHandler = nullptr; }
+
+    /**
+     * This method sets the callback handler function pointer and its associated context.
+     *
+     * @param[in] aHandler   The handler function pointer.
+     * @param[in] aContext   The context associated with handler.
+     *
+     */
+    void Set(HandlerType aHandler, void *aContext)
+    {
+        mHandler = aHandler;
+        mContext = aContext;
+    }
+
+    /**
+     * This method indicates whether or not the callback is set (not `nullptr`).
+     *
+     * @retval TRUE   The handler is set.
+     * @retval FALSE  The handler is not set.
+     *
+     */
+    bool IsSet(void) const { return (mHandler != nullptr); }
+
+    /**
+     * This method returns the handler function pointer.
+     *
+     * @returns The handler function pointer.
+     *
+     */
+    HandlerType GetHandler(void) const { return mHandler; }
+
+    /**
+     * This method returns the context associated with callback.
+     *
+     * @returns The context.
+     *
+     */
+    void *GetContext(void) const { return mContext; }
+
+    /**
+     * This method indicates whether the callback matches a given handler function pointer and context.
+     *
+     * @param[in] aHandler   The handler function pointer to compare with.
+     * @param[in] aContext   The context associated with handler.
+     *
+     * @retval TRUE   The callback matches @p aHandler and @p aContext.
+     * @retval FALSE  The callback does not match @p aHandler and @p aContext.
+     *
+     */
+    bool Matches(HandlerType aHandler, void *aContext) const
+    {
+        return (mHandler == aHandler) && (mContext == aContext);
+    }
+
+protected:
+    CallbackBase(void)
+        : mHandler(nullptr)
+        , mContext(nullptr)
+    {
+    }
+
+    HandlerType mHandler;
+    void       *mContext;
+};
+
+/**
+ * This class represents a `Callback` (a function pointer handler and a `void *` context).
+ *
+ * The context is passed as one of the arguments to the function pointer handler when invoked.
+ *
+ * The `Callback` provides two specializations based on `CallbackContextPosition` in the function pointer, i.e., whether
+ * it is passed as the first argument or as the last argument.
+ *
+ * The `CallbackContextPosition` template parameter is automatically determined at compile-time based on the given
+ * `HandlerType`. So user can simply use `Callback<HandlerType>`. The `Invoke()` method will properly pass the context
+ * to the function handler.
+ *
+ * @tparam  HandlerType                The function pointer handler type.
+ * @tparam  CallbackContextPosition    Context position (first or last). Automatically determined at compile-time.
+ *
+ */
+template <typename HandlerType,
+          CallbackContextPosition =
+              (TypeTraits::IsSame<typename TypeTraits::FirstArgTypeOf<HandlerType>::Type, void *>::kValue
+                   ? kContextAsFirstArg
+                   : kContextAsLastArg)>
+class Callback
+{
+};
+
+// Specialization for `kContextAsLastArg`
+template <typename HandlerType> class Callback<HandlerType, kContextAsLastArg> : public CallbackBase<HandlerType>
+{
+    using CallbackBase<HandlerType>::mHandler;
+    using CallbackBase<HandlerType>::mContext;
+
+public:
+    using ReturnType = typename TypeTraits::ReturnTypeOf<HandlerType>::Type; ///< Return type of `HandlerType`.
+
+    static constexpr CallbackContextPosition kContextPosition = kContextAsLastArg; ///< Context position.
+
+    /**
+     * This constructor initializes `Callback` as empty (`nullptr` handler function pointer).
+     *
+     */
+    Callback(void) = default;
+
+    /**
+     * This method invokes the callback handler.
+     *
+     * The caller MUST ensure that callback is set (`IsSet()` returns `true`) before calling this method.
+     *
+     * @param[in] aArgs   The args to pass to the callback handler.
+     *
+     * @returns The return value from handler.
+     *
+     */
+    template <typename... Args> ReturnType Invoke(Args &&...aArgs) const
+    {
+        return mHandler(static_cast<Args &&>(aArgs)..., mContext);
+    }
+
+    /**
+     * This method invokes the callback handler if it is set.
+     *
+     * The method MUST be used when the handler function returns `void`.
+     *
+     * @param[in] aArgs   The args to pass to the callback handler.
+     *
+     */
+    template <typename... Args> void InvokeIfSet(Args &&...aArgs) const
+    {
+        static_assert(TypeTraits::IsSame<ReturnType, void>::kValue,
+                      "InvokeIfSet() MUST be used with `void` returning handler");
+
+        if (mHandler != nullptr)
+        {
+            Invoke(static_cast<Args &&>(aArgs)...);
+        }
+    }
+};
+
+// Specialization for `kContextAsFirstArg`
+template <typename HandlerType> class Callback<HandlerType, kContextAsFirstArg> : public CallbackBase<HandlerType>
+{
+    using CallbackBase<HandlerType>::mHandler;
+    using CallbackBase<HandlerType>::mContext;
+
+public:
+    using ReturnType = typename TypeTraits::ReturnTypeOf<HandlerType>::Type;
+
+    static constexpr CallbackContextPosition kContextPosition = kContextAsFirstArg;
+
+    Callback(void) = default;
+
+    template <typename... Args> ReturnType Invoke(Args &&...aArgs) const
+    {
+        return mHandler(mContext, static_cast<Args &&>(aArgs)...);
+    }
+
+    template <typename... Args> void InvokeIfSet(Args &&...aArgs) const
+    {
+        static_assert(TypeTraits::IsSame<ReturnType, void>::kValue,
+                      "InvokeIfSet() MUST be used with `void` returning handler");
+
+        if (mHandler != nullptr)
+        {
+            Invoke(static_cast<Args &&>(aArgs)...);
+        }
+    }
+};
+
+} // namespace ot
+
+#endif // CALLBACK_HPP_
diff --git a/src/core/common/clearable.hpp b/src/core/common/clearable.hpp
index 2cef091..b47bb19 100644
--- a/src/core/common/clearable.hpp
+++ b/src/core/common/clearable.hpp
@@ -41,7 +41,7 @@
 namespace ot {
 
 /**
- * This template class defines a Clearable object which provides `Clear()` method.
+ * This template class defines a `Clearable` object which provides `Clear()` method.
  *
  * The `Clear` implementation simply sets all the bytes of a `Type` instance to zero.
  *
@@ -52,7 +52,7 @@
 template <typename Type> class Clearable
 {
 public:
-    void Clear(void) { memset(reinterpret_cast<void *>(this), 0, sizeof(Type)); }
+    void Clear(void) { memset(reinterpret_cast<void *>(static_cast<Type *>(this)), 0, sizeof(Type)); }
 };
 
 } // namespace ot
diff --git a/src/core/common/code_utils.hpp b/src/core/common/code_utils.hpp
index 78e01c6..379c951 100644
--- a/src/core/common/code_utils.hpp
+++ b/src/core/common/code_utils.hpp
@@ -170,9 +170,6 @@
  * @param[in]  aError  The error to be ignored.
  *
  */
-static inline void IgnoreError(otError aError)
-{
-    OT_UNUSED_VARIABLE(aError);
-}
+static inline void IgnoreError(otError aError) { OT_UNUSED_VARIABLE(aError); }
 
 #endif // CODE_UTILS_HPP_
diff --git a/src/core/common/const_cast.hpp b/src/core/common/const_cast.hpp
index 54fd2ff..2927df3 100644
--- a/src/core/common/const_cast.hpp
+++ b/src/core/common/const_cast.hpp
@@ -48,10 +48,7 @@
  * @returns A const reference to @p aObject reference.
  *
  */
-template <typename Type> const Type &AsConst(Type &aObject)
-{
-    return const_cast<const Type &>(aObject);
-}
+template <typename Type> const Type &AsConst(Type &aObject) { return const_cast<const Type &>(aObject); }
 
 /**
  * This template method casts a given non-const pointer to a const pointer.
@@ -63,10 +60,7 @@
  * @returns A const pointer to @p aPointer pointer.
  *
  */
-template <typename Type> const Type *AsConst(Type *aPointer)
-{
-    return const_cast<const Type *>(aPointer);
-}
+template <typename Type> const Type *AsConst(Type *aPointer) { return const_cast<const Type *>(aPointer); }
 
 /**
  * This template method casts a given const reference to a non-const reference.
@@ -78,10 +72,7 @@
  * @returns A non-const reference to @p aObject reference.
  *
  */
-template <typename Type> Type &AsNonConst(const Type &aObject)
-{
-    return const_cast<Type &>(aObject);
-}
+template <typename Type> Type &AsNonConst(const Type &aObject) { return const_cast<Type &>(aObject); }
 
 /**
  * This template method casts a given const pointer to a non-const pointer.
@@ -93,10 +84,7 @@
  * @returns A non-const pointer to @p aPointer pointer.
  *
  */
-template <typename Type> Type *AsNonConst(const Type *aPointer)
-{
-    return const_cast<Type *>(aPointer);
-}
+template <typename Type> Type *AsNonConst(const Type *aPointer) { return const_cast<Type *>(aPointer); }
 
 } // namespace ot
 
diff --git a/src/core/common/data.hpp b/src/core/common/data.hpp
index 59b37a3..404e6a3 100644
--- a/src/core/common/data.hpp
+++ b/src/core/common/data.hpp
@@ -44,6 +44,7 @@
 #include "common/const_cast.hpp"
 #include "common/equatable.hpp"
 #include "common/error.hpp"
+#include "common/num_utils.hpp"
 #include "common/type_traits.hpp"
 
 namespace ot {
@@ -360,7 +361,7 @@
     {
         Error error = (mLength >= aLength) ? kErrorNone : kErrorNoBufs;
 
-        mLength = OT_MIN(mLength, aLength);
+        mLength = Min(mLength, aLength);
         memcpy(AsNonConst(mBuffer), aBuffer, mLength);
 
         return error;
diff --git a/src/core/common/debug.hpp b/src/core/common/debug.hpp
index 4febe34..8bb2199 100644
--- a/src/core/common/debug.hpp
+++ b/src/core/common/debug.hpp
@@ -108,4 +108,19 @@
         }                        \
     } while (false)
 
+/**
+ * @def AssertPointerIsNotNull
+ *
+ * This macro asserts that a given pointer (API input parameter) is not `nullptr`. This macro checks the pointer only
+ * when `OPENTHREAD_CONFIG_ASSERT_CHECK_API_POINTER_PARAM_FOR_NULL` is enabled. Otherwise it is an empty macro.
+ *
+ * @param[in]  aPointer   The pointer variable (API input parameter) to check.
+ *
+ */
+#if OPENTHREAD_CONFIG_ASSERT_CHECK_API_POINTER_PARAM_FOR_NULL
+#define AssertPointerIsNotNull(aPointer) OT_ASSERT((aPointer) != nullptr)
+#else
+#define AssertPointerIsNotNull(aPointer)
+#endif
+
 #endif // DEBUG_HPP_
diff --git a/src/core/common/encoding.hpp b/src/core/common/encoding.hpp
index b67d779..29e66d5 100644
--- a/src/core/common/encoding.hpp
+++ b/src/core/common/encoding.hpp
@@ -51,10 +51,7 @@
 namespace ot {
 namespace Encoding {
 
-inline uint16_t Swap16(uint16_t v)
-{
-    return (((v & 0x00ffU) << 8) & 0xff00) | (((v & 0xff00U) >> 8) & 0x00ff);
-}
+inline uint16_t Swap16(uint16_t v) { return (((v & 0x00ffU) << 8) & 0xff00) | (((v & 0xff00U) >> 8) & 0x00ff); }
 
 inline uint32_t Swap32(uint32_t v)
 {
@@ -91,33 +88,15 @@
 
 #if BYTE_ORDER_BIG_ENDIAN
 
-inline uint16_t HostSwap16(uint16_t v)
-{
-    return v;
-}
-inline uint32_t HostSwap32(uint32_t v)
-{
-    return v;
-}
-inline uint64_t HostSwap64(uint64_t v)
-{
-    return v;
-}
+inline uint16_t HostSwap16(uint16_t v) { return v; }
+inline uint32_t HostSwap32(uint32_t v) { return v; }
+inline uint64_t HostSwap64(uint64_t v) { return v; }
 
 #else /* BYTE_ORDER_LITTLE_ENDIAN */
 
-inline uint16_t HostSwap16(uint16_t v)
-{
-    return Swap16(v);
-}
-inline uint32_t HostSwap32(uint32_t v)
-{
-    return Swap32(v);
-}
-inline uint64_t HostSwap64(uint64_t v)
-{
-    return Swap64(v);
-}
+inline uint16_t HostSwap16(uint16_t v) { return Swap16(v); }
+inline uint32_t HostSwap32(uint32_t v) { return Swap32(v); }
+inline uint64_t HostSwap64(uint64_t v) { return Swap64(v); }
 
 #endif // LITTLE_ENDIAN
 
@@ -133,22 +112,10 @@
  */
 template <typename UintType> UintType HostSwap(UintType aValue);
 
-template <> inline uint8_t HostSwap(uint8_t aValue)
-{
-    return aValue;
-}
-template <> inline uint16_t HostSwap(uint16_t aValue)
-{
-    return HostSwap16(aValue);
-}
-template <> inline uint32_t HostSwap(uint32_t aValue)
-{
-    return HostSwap32(aValue);
-}
-template <> inline uint64_t HostSwap(uint64_t aValue)
-{
-    return HostSwap64(aValue);
-}
+template <> inline uint8_t  HostSwap(uint8_t aValue) { return aValue; }
+template <> inline uint16_t HostSwap(uint16_t aValue) { return HostSwap16(aValue); }
+template <> inline uint32_t HostSwap(uint32_t aValue) { return HostSwap32(aValue); }
+template <> inline uint64_t HostSwap(uint64_t aValue) { return HostSwap64(aValue); }
 
 /**
  * This function reads a `uint16_t` value from a given buffer assuming big-endian encoding.
@@ -158,10 +125,7 @@
  * @returns The `uint16_t` value read from buffer.
  *
  */
-inline uint16_t ReadUint16(const uint8_t *aBuffer)
-{
-    return static_cast<uint16_t>((aBuffer[0] << 8) | aBuffer[1]);
-}
+inline uint16_t ReadUint16(const uint8_t *aBuffer) { return static_cast<uint16_t>((aBuffer[0] << 8) | aBuffer[1]); }
 
 /**
  * This function reads a `uint32_t` value from a given buffer assuming big-endian encoding.
@@ -274,33 +238,15 @@
 
 #if BYTE_ORDER_BIG_ENDIAN
 
-inline uint16_t HostSwap16(uint16_t v)
-{
-    return Swap16(v);
-}
-inline uint32_t HostSwap32(uint32_t v)
-{
-    return Swap32(v);
-}
-inline uint64_t HostSwap64(uint64_t v)
-{
-    return Swap64(v);
-}
+inline uint16_t HostSwap16(uint16_t v) { return Swap16(v); }
+inline uint32_t HostSwap32(uint32_t v) { return Swap32(v); }
+inline uint64_t HostSwap64(uint64_t v) { return Swap64(v); }
 
 #else /* BYTE_ORDER_LITTLE_ENDIAN */
 
-inline uint16_t HostSwap16(uint16_t v)
-{
-    return v;
-}
-inline uint32_t HostSwap32(uint32_t v)
-{
-    return v;
-}
-inline uint64_t HostSwap64(uint64_t v)
-{
-    return v;
-}
+inline uint16_t HostSwap16(uint16_t v) { return v; }
+inline uint32_t HostSwap32(uint32_t v) { return v; }
+inline uint64_t HostSwap64(uint64_t v) { return v; }
 
 #endif
 
@@ -316,22 +262,10 @@
  */
 template <typename UintType> UintType HostSwap(UintType aValue);
 
-template <> inline uint8_t HostSwap(uint8_t aValue)
-{
-    return aValue;
-}
-template <> inline uint16_t HostSwap(uint16_t aValue)
-{
-    return HostSwap16(aValue);
-}
-template <> inline uint32_t HostSwap(uint32_t aValue)
-{
-    return HostSwap32(aValue);
-}
-template <> inline uint64_t HostSwap(uint64_t aValue)
-{
-    return HostSwap64(aValue);
-}
+template <> inline uint8_t  HostSwap(uint8_t aValue) { return aValue; }
+template <> inline uint16_t HostSwap(uint16_t aValue) { return HostSwap16(aValue); }
+template <> inline uint32_t HostSwap(uint32_t aValue) { return HostSwap32(aValue); }
+template <> inline uint64_t HostSwap(uint64_t aValue) { return HostSwap64(aValue); }
 
 /**
  * This function reads a `uint16_t` value from a given buffer assuming little-endian encoding.
@@ -341,10 +275,7 @@
  * @returns The `uint16_t` value read from buffer.
  *
  */
-inline uint16_t ReadUint16(const uint8_t *aBuffer)
-{
-    return static_cast<uint16_t>(aBuffer[0] | (aBuffer[1] << 8));
-}
+inline uint16_t ReadUint16(const uint8_t *aBuffer) { return static_cast<uint16_t>(aBuffer[0] | (aBuffer[1] << 8)); }
 
 /**
  * This function reads a 24-bit integer value from a given buffer assuming little-endian encoding.
diff --git a/src/core/common/equatable.hpp b/src/core/common/equatable.hpp
index 06fdda9..d913015 100644
--- a/src/core/common/equatable.hpp
+++ b/src/core/common/equatable.hpp
@@ -87,7 +87,10 @@
      * @retval FALSE  If the two `Type` instances are not equal.
      *
      */
-    bool operator==(const Type &aOther) const { return memcmp(this, &aOther, sizeof(Type)) == 0; }
+    bool operator==(const Type &aOther) const
+    {
+        return memcmp(static_cast<const Type *>(this), &aOther, sizeof(Type)) == 0;
+    }
 };
 
 } // namespace ot
diff --git a/src/core/common/frame_builder.cpp b/src/core/common/frame_builder.cpp
index 6d95d1a..88247ca 100644
--- a/src/core/common/frame_builder.cpp
+++ b/src/core/common/frame_builder.cpp
@@ -33,6 +33,16 @@
 
 #include "frame_builder.hpp"
 
+#include <string.h>
+
+#include "common/code_utils.hpp"
+#include "common/debug.hpp"
+#include "common/encoding.hpp"
+
+#if OPENTHREAD_FTD || OPENTHREAD_MTD
+#include "common/message.hpp"
+#endif
+
 namespace ot {
 
 void FrameBuilder::Init(void *aBuffer, uint16_t aLength)
@@ -42,10 +52,7 @@
     mMaxLength = aLength;
 }
 
-Error FrameBuilder::AppendUint8(uint8_t aUint8)
-{
-    return Append<uint8_t>(aUint8);
-}
+Error FrameBuilder::AppendUint8(uint8_t aUint8) { return Append<uint8_t>(aUint8); }
 
 Error FrameBuilder::AppendBigEndianUint16(uint16_t aUint16)
 {
@@ -79,6 +86,31 @@
     return error;
 }
 
+Error FrameBuilder::AppendMacAddress(const Mac::Address &aMacAddress)
+{
+    Error error = kErrorNone;
+
+    switch (aMacAddress.GetType())
+    {
+    case Mac::Address::kTypeNone:
+        break;
+
+    case Mac::Address::kTypeShort:
+        error = AppendLittleEndianUint16(aMacAddress.GetShort());
+        break;
+
+    case Mac::Address::kTypeExtended:
+        VerifyOrExit(CanAppend(sizeof(Mac::ExtAddress)), error = kErrorNoBufs);
+        aMacAddress.GetExtended().CopyTo(mBuffer + mLength, Mac::ExtAddress::kReverseByteOrder);
+        mLength += sizeof(Mac::ExtAddress);
+        break;
+    }
+
+exit:
+    return error;
+}
+
+#if OPENTHREAD_FTD || OPENTHREAD_MTD
 Error FrameBuilder::AppendBytesFromMessage(const Message &aMessage, uint16_t aOffset, uint16_t aLength)
 {
     Error error = kErrorNone;
@@ -90,10 +122,33 @@
 exit:
     return error;
 }
+#endif
 
 void FrameBuilder::WriteBytes(uint16_t aOffset, const void *aBuffer, uint16_t aLength)
 {
     memcpy(mBuffer + aOffset, aBuffer, aLength);
 }
 
+Error FrameBuilder::InsertBytes(uint16_t aOffset, const void *aBuffer, uint16_t aLength)
+{
+    Error error = kErrorNone;
+
+    OT_ASSERT(aOffset <= mLength);
+
+    VerifyOrExit(CanAppend(aLength), error = kErrorNoBufs);
+
+    memmove(mBuffer + aOffset + aLength, mBuffer + aOffset, mLength - aOffset);
+    memcpy(mBuffer + aOffset, aBuffer, aLength);
+    mLength += aLength;
+
+exit:
+    return error;
+}
+
+void FrameBuilder::RemoveBytes(uint16_t aOffset, uint16_t aLength)
+{
+    memmove(mBuffer + aOffset, mBuffer + aOffset + aLength, mLength - aOffset - aLength);
+    mLength -= aLength;
+}
+
 } // namespace ot
diff --git a/src/core/common/frame_builder.hpp b/src/core/common/frame_builder.hpp
index 8115045..bbbb731 100644
--- a/src/core/common/frame_builder.hpp
+++ b/src/core/common/frame_builder.hpp
@@ -37,10 +37,11 @@
 #include "openthread-core-config.h"
 
 #include "common/error.hpp"
-#include "common/message.hpp"
 #include "common/type_traits.hpp"
+#include "mac/mac_types.hpp"
 
 namespace ot {
+class Message;
 
 /**
  * The `FrameBuilder` can be used to construct frame content in a given data buffer.
@@ -77,7 +78,7 @@
     uint16_t GetLength(void) const { return mLength; }
 
     /**
-     * This method returns the maximum length of frame.
+     * This method returns the maximum length of the frame.
      *
      * @returns The maximum frame length (max number of bytes in the frame buffer).
      *
@@ -85,6 +86,25 @@
     uint16_t GetMaxLength(void) const { return mMaxLength; }
 
     /**
+     * This method sets the maximum length of the frame.
+     *
+     * This method does not perform any checks on the new given length. The caller MUST ensure that the specified max
+     * length is valid for the frame buffer.
+     *
+     * @param[in] aLength  The maximum frame length.
+     *
+     */
+    void SetMaxLength(uint16_t aLength) { mMaxLength = aLength; }
+
+    /**
+     * This method returns the remaining length (number of bytes that can be appended) in the frame.
+     *
+     * @returns The remaining length.
+     *
+     */
+    uint16_t GetRemainingLength(void) const { return mMaxLength - mLength; }
+
+    /**
      * This method indicates whether or not there are enough bytes remaining in the `FrameBuilder` buffer to append a
      * given number of bytes.
      *
@@ -164,6 +184,18 @@
     Error AppendBytes(const void *aBuffer, uint16_t aLength);
 
     /**
+     * This method appends a given `Mac::Address` to the `FrameBuilder`.
+     *
+     * @param[in] aMacAddress  A `Mac::Address` to append.
+     *
+     * @retval kErrorNone    Successfully appended the address.
+     * @retval kErrorNoBufs  Insufficient available buffers.
+     *
+     */
+    Error AppendMacAddress(const Mac::Address &aMacAddress);
+
+#if OPENTHREAD_FTD || OPENTHREAD_MTD
+    /**
      * This method appends bytes read from a given message to the `FrameBuilder`.
      *
      * @param[in] aMessage   The message to read the bytes from.
@@ -176,6 +208,7 @@
      *
      */
     Error AppendBytesFromMessage(const Message &aMessage, uint16_t aOffset, uint16_t aLength);
+#endif
 
     /**
      * This method appends an object to the `FrameBuilder`.
@@ -227,6 +260,57 @@
         WriteBytes(aOffset, &aObject, sizeof(ObjectType));
     }
 
+    /**
+     * This method inserts bytes in `FrameBuilder` at a given offset, moving previous content forward.
+     *
+     * The caller MUST ensure that @p aOffset is within the current frame length (from 0 up to and including
+     * `GetLength()`). Otherwise the behavior of this method is undefined.
+     *
+     * @param[in] aOffset   The offset to insert bytes.
+     * @param[in] aBuffer   A pointer to a data buffer to insert.
+     * @param[in] aLength   Number of bytes in @p aBuffer.
+     *
+     * @retval kErrorNone    Successfully inserted the bytes.
+     * @retval kErrorNoBufs  Insufficient available buffers to insert the bytes.
+     *
+     */
+    Error InsertBytes(uint16_t aOffset, const void *aBuffer, uint16_t aLength);
+
+    /**
+     * This method inserts an object in `FrameBuilder` at a given offset, moving previous content forward.
+     *
+     * The caller MUST ensure that @p aOffset is within the current frame length (from 0 up to and including
+     * `GetLength()`). Otherwise the behavior of this method is undefined.
+     *
+     * @tparam     ObjectType   The object type to insert.
+     *
+     * @param[in]  aOffset      The offset to insert bytes.
+     * @param[in]  aObject      A reference to the object to insert.
+     *
+     * @retval kErrorNone       Successfully inserted the bytes.
+     * @retval kErrorNoBufs     Insufficient available buffers to insert the bytes.
+     *
+     */
+    template <typename ObjectType> Error Insert(uint16_t aOffset, const ObjectType &aObject)
+    {
+        static_assert(!TypeTraits::IsPointer<ObjectType>::kValue, "ObjectType must not be a pointer");
+
+        return InsertBytes(aOffset, &aObject, sizeof(ObjectType));
+    }
+
+    /**
+     * This method removes a given number of bytes in `FrameBuilder` at a given offset, moving existing content
+     * after removed bytes backward.
+     *
+     * This method does not perform any bound checks. The caller MUST ensure that the given length and offset fits
+     * within the previously appended content. Otherwise the behavior of this method is undefined.
+     *
+     * @param[in] aOffset   The offset to remove bytes from.
+     * @param[in] aLength   The number of bytes to remove.
+     *
+     */
+    void RemoveBytes(uint16_t aOffset, uint16_t aLength);
+
 private:
     uint8_t *mBuffer;
     uint16_t mLength;
diff --git a/src/core/common/frame_data.cpp b/src/core/common/frame_data.cpp
index 7607041..04d2aa9 100644
--- a/src/core/common/frame_data.cpp
+++ b/src/core/common/frame_data.cpp
@@ -38,10 +38,7 @@
 
 namespace ot {
 
-Error FrameData::ReadUint8(uint8_t &aUint8)
-{
-    return ReadBytes(&aUint8, sizeof(uint8_t));
-}
+Error FrameData::ReadUint8(uint8_t &aUint8) { return ReadBytes(&aUint8, sizeof(uint8_t)); }
 
 Error FrameData::ReadBigEndianUint16(uint16_t &aUint16)
 {
@@ -99,9 +96,6 @@
     return error;
 }
 
-void FrameData::SkipOver(uint16_t aLength)
-{
-    Init(GetBytes() + aLength, GetLength() - aLength);
-}
+void FrameData::SkipOver(uint16_t aLength) { Init(GetBytes() + aLength, GetLength() - aLength); }
 
 } // namespace ot
diff --git a/src/core/common/heap.cpp b/src/core/common/heap.cpp
index 77a171a..7b431ee 100644
--- a/src/core/common/heap.cpp
+++ b/src/core/common/heap.cpp
@@ -40,27 +40,15 @@
 
 #if OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
 
-void *CAlloc(size_t aCount, size_t aSize)
-{
-    return otPlatCAlloc(aCount, aSize);
-}
+void *CAlloc(size_t aCount, size_t aSize) { return otPlatCAlloc(aCount, aSize); }
 
-void Free(void *aPointer)
-{
-    otPlatFree(aPointer);
-}
+void Free(void *aPointer) { otPlatFree(aPointer); }
 
 #else
 
-void *CAlloc(size_t aCount, size_t aSize)
-{
-    return Instance::GetHeap().CAlloc(aCount, aSize);
-}
+void *CAlloc(size_t aCount, size_t aSize) { return Instance::GetHeap().CAlloc(aCount, aSize); }
 
-void Free(void *aPointer)
-{
-    Instance::GetHeap().Free(aPointer);
-}
+void Free(void *aPointer) { Instance::GetHeap().Free(aPointer); }
 
 #endif // OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
 
diff --git a/src/core/common/heap_allocatable.hpp b/src/core/common/heap_allocatable.hpp
index 1b88016..e9073ed 100644
--- a/src/core/common/heap_allocatable.hpp
+++ b/src/core/common/heap_allocatable.hpp
@@ -70,7 +70,7 @@
      * @returns A pointer to the newly allocated instance or `nullptr` if it fails to allocate.
      *
      */
-    template <typename... Args> static Type *Allocate(Args &&... aArgs)
+    template <typename... Args> static Type *Allocate(Args &&...aArgs)
     {
         void *buf = Heap::CAlloc(1, sizeof(Type));
 
@@ -89,7 +89,7 @@
      * @returns A pointer to the newly allocated instance or `nullptr` if it fails to allocate or initialize.
      *
      */
-    template <typename... Args> static Type *AllocateAndInit(Args &&... aArgs)
+    template <typename... Args> static Type *AllocateAndInit(Args &&...aArgs)
     {
         void *buf    = Heap::CAlloc(1, sizeof(Type));
         Type *object = nullptr;
diff --git a/src/core/common/heap_array.hpp b/src/core/common/heap_array.hpp
index a4ede90..29645f6 100644
--- a/src/core/common/heap_array.hpp
+++ b/src/core/common/heap_array.hpp
@@ -506,12 +506,12 @@
     // loop iteration over the array elements and should not be used
     // directly.
 
-    Type *      begin(void) { return (mLength > 0) ? mArray : nullptr; }
-    Type *      end(void) { return (mLength > 0) ? &mArray[mLength] : nullptr; }
+    Type       *begin(void) { return (mLength > 0) ? mArray : nullptr; }
+    Type       *end(void) { return (mLength > 0) ? &mArray[mLength] : nullptr; }
     const Type *begin(void) const { return (mLength > 0) ? mArray : nullptr; }
     const Type *end(void) const { return (mLength > 0) ? &mArray[mLength] : nullptr; }
 
-    Array(const Array &) = delete;
+    Array(const Array &)            = delete;
     Array &operator=(const Array &) = delete;
 
 private:
@@ -538,7 +538,7 @@
         return error;
     }
 
-    Type *    mArray;
+    Type     *mArray;
     IndexType mLength;
     IndexType mCapacity;
 };
diff --git a/src/core/common/heap_data.hpp b/src/core/common/heap_data.hpp
index 041f539..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(); }
@@ -178,7 +178,7 @@
      */
     void Free(void);
 
-    Data(const Data &) = delete;
+    Data(const Data &)            = delete;
     Data &operator=(const Data &) = delete;
 
 private:
diff --git a/src/core/common/heap_string.hpp b/src/core/common/heap_string.hpp
index c9a27fd..2f737bb 100644
--- a/src/core/common/heap_string.hpp
+++ b/src/core/common/heap_string.hpp
@@ -175,7 +175,7 @@
      */
     bool operator==(const String &aString) const { return (*this == aString.AsCString()); }
 
-    String(const String &) = delete;
+    String(const String &)            = delete;
     String &operator=(const String &) = delete;
 
 private:
diff --git a/src/core/common/instance.cpp b/src/core/common/instance.cpp
index 6aa1075..d0aebec 100644
--- a/src/core/common/instance.cpp
+++ b/src/core/common/instance.cpp
@@ -50,7 +50,8 @@
 
 #if OPENTHREAD_MTD || OPENTHREAD_FTD
 #if !OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
-Utils::Heap Instance::sHeap;
+OT_DEFINE_ALIGNED_VAR(sHeapRaw, sizeof(Utils::Heap), uint64_t);
+Utils::Heap *Instance::sHeap{nullptr};
 #endif
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
 bool Instance::sDnsNameCompressionEnabled = true;
@@ -135,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)
@@ -145,7 +147,7 @@
     , mCommissioner(*this)
 #endif
 #if OPENTHREAD_CONFIG_DTLS_ENABLE
-    , mCoapSecure(*this)
+    , mTmfSecureAgent(*this)
 #endif
 #if OPENTHREAD_CONFIG_JOINER_ENABLE
     , mJoiner(*this)
@@ -174,13 +176,10 @@
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
     , mSrpServer(*this)
 #endif
-
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
 #if OPENTHREAD_FTD
     , mChildSupervisor(*this)
 #endif
     , mSupervisionListener(*this)
-#endif
     , mAnnounceBegin(*this)
     , mPanIdQuery(*this)
     , mEnergyScan(*this)
@@ -190,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)
@@ -208,6 +210,9 @@
 #if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
     , mChannelManager(*this)
 #endif
+#if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
+    , mMeshDiag(*this)
+#endif
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
     , mHistoryTracker(*this)
 #endif
@@ -223,6 +228,9 @@
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     , mRoutingManager(*this)
 #endif
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    , mNat64Translator(*this)
+#endif
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
 #if OPENTHREAD_RADIO || OPENTHREAD_CONFIG_LINK_RAW_ENABLE
     , mLinkRaw(*this)
@@ -233,10 +241,25 @@
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
     , mDiags(*this)
 #endif
+#if OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+    , mPowerCalibration(*this)
+#endif
     , mIsInitialized(false)
 {
 }
 
+#if (OPENTHREAD_MTD || OPENTHREAD_FTD) && !OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
+Utils::Heap &Instance::GetHeap(void)
+{
+    if (nullptr == sHeap)
+    {
+        sHeap = new (&sHeapRaw) Utils::Heap();
+    }
+
+    return *sHeap;
+}
+#endif
+
 #if !OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE
 
 Instance &Instance::InitSingle(void)
@@ -283,10 +306,7 @@
 
 #endif // OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE
 
-void Instance::Reset(void)
-{
-    otPlatReset(this);
-}
+void Instance::Reset(void) { otPlatReset(this); }
 
 #if OPENTHREAD_RADIO
 void Instance::ResetRadioStack(void)
@@ -328,6 +348,10 @@
     IgnoreError(otIp6SetEnabled(this, false));
     IgnoreError(otLinkSetEnabled(this, false));
 
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    Get<KeyManager>().DestroyTemporaryKeys();
+#endif
+
     Get<Settings>().Deinit();
 #endif
 
@@ -352,6 +376,10 @@
 void Instance::FactoryReset(void)
 {
     Get<Settings>().Wipe();
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    Get<KeyManager>().DestroyTemporaryKeys();
+    Get<KeyManager>().DestroyPersistentKeys();
+#endif
     otPlatReset(this);
 }
 
@@ -361,6 +389,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;
@@ -370,8 +402,9 @@
 {
     aInfo.Clear();
 
-    aInfo.mTotalBuffers = Get<MessagePool>().GetTotalBufferCount();
-    aInfo.mFreeBuffers  = Get<MessagePool>().GetFreeBufferCount();
+    aInfo.mTotalBuffers   = Get<MessagePool>().GetTotalBufferCount();
+    aInfo.mFreeBuffers    = Get<MessagePool>().GetFreeBufferCount();
+    aInfo.mMaxUsedBuffers = Get<MessagePool>().GetMaxUsedBufferCount();
 
     Get<MeshForwarder>().GetSendQueue().GetInfo(aInfo.m6loSendQueue);
     Get<MeshForwarder>().GetReassemblyQueue().GetInfo(aInfo.m6loReassemblyQueue);
@@ -387,8 +420,8 @@
     Get<Tmf::Agent>().GetCachedResponses().GetInfo(aInfo.mCoapQueue);
 
 #if OPENTHREAD_CONFIG_DTLS_ENABLE
-    Get<Coap::CoapSecure>().GetRequestMessages().GetInfo(aInfo.mCoapSecureQueue);
-    Get<Coap::CoapSecure>().GetCachedResponses().GetInfo(aInfo.mCoapSecureQueue);
+    Get<Tmf::SecureAgent>().GetRequestMessages().GetInfo(aInfo.mCoapSecureQueue);
+    Get<Tmf::SecureAgent>().GetCachedResponses().GetInfo(aInfo.mCoapSecureQueue);
 #endif
 
 #if OPENTHREAD_CONFIG_COAP_API_ENABLE
@@ -397,6 +430,8 @@
 #endif
 }
 
+void Instance::ResetBufferInfo(void) { Get<MessagePool>().ResetMaxUsedBufferCount(); }
+
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
 
 #if OPENTHREAD_CONFIG_LOG_LEVEL_DYNAMIC_ENABLE
@@ -410,10 +445,7 @@
     }
 }
 
-extern "C" OT_TOOL_WEAK void otPlatLogHandleLevelChanged(otLogLevel aLogLevel)
-{
-    OT_UNUSED_VARIABLE(aLogLevel);
-}
+extern "C" OT_TOOL_WEAK void otPlatLogHandleLevelChanged(otLogLevel aLogLevel) { OT_UNUSED_VARIABLE(aLogLevel); }
 
 #endif
 
diff --git a/src/core/common/instance.hpp b/src/core/common/instance.hpp
index dd3e4e6..8ce6eba 100644
--- a/src/core/common/instance.hpp
+++ b/src/core/common/instance.hpp
@@ -60,6 +60,7 @@
 #include "mac/link_raw.hpp"
 #include "radio/radio.hpp"
 #include "utils/otns.hpp"
+#include "utils/power_calibration.hpp"
 
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
 #include "backbone_router/backbone_tmf.hpp"
@@ -90,6 +91,7 @@
 #include "net/dnssd_server.hpp"
 #include "net/ip6.hpp"
 #include "net/ip6_filter.hpp"
+#include "net/nat64_translator.hpp"
 #include "net/nd_agent.hpp"
 #include "net/netif.hpp"
 #include "net/sntp_client.hpp"
@@ -99,6 +101,7 @@
 #include "thread/announce_begin_server.hpp"
 #include "thread/announce_sender.hpp"
 #include "thread/anycast_locator.hpp"
+#include "thread/child_supervision.hpp"
 #include "thread/discover_scanner.hpp"
 #include "thread/dua_manager.hpp"
 #include "thread/energy_scan_server.hpp"
@@ -121,10 +124,10 @@
 #include "thread/tmf.hpp"
 #include "utils/channel_manager.hpp"
 #include "utils/channel_monitor.hpp"
-#include "utils/child_supervision.hpp"
 #include "utils/heap.hpp"
 #include "utils/history_tracker.hpp"
 #include "utils/jam_detector.hpp"
+#include "utils/mesh_diag.hpp"
 #include "utils/ping_sender.hpp"
 #include "utils/slaac_address.hpp"
 #include "utils/srp_client_buffers.hpp"
@@ -289,7 +292,7 @@
      * @returns A reference to the Heap object.
      *
      */
-    static Utils::Heap &GetHeap(void) { return sHeap; }
+    static Utils::Heap &GetHeap(void);
 #endif
 
 #if OPENTHREAD_CONFIG_COAP_API_ENABLE
@@ -343,6 +346,15 @@
      */
     void GetBufferInfo(BufferInfo &aInfo);
 
+    /**
+     * This method resets the Message Buffer information counter tracking maximum number buffers in use at the same
+     * time.
+     *
+     * This method resets `mMaxUsedBuffers` in `BufferInfo`.
+     *
+     */
+    void ResetBufferInfo(void);
+
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
 
     /**
@@ -380,7 +392,7 @@
     // Random::Manager is initialized before other objects. Note that it
     // requires MbedTls which itself may use Heap.
 #if !OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
-    static Utils::Heap sHeap;
+    static Utils::Heap *sHeap;
 #endif
     Crypto::MbedTls mMbedTls;
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
@@ -483,8 +495,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
@@ -496,7 +509,7 @@
 #endif
 
 #if OPENTHREAD_CONFIG_DTLS_ENABLE
-    Coap::CoapSecure mCoapSecure;
+    Tmf::SecureAgent mTmfSecureAgent;
 #endif
 
 #if OPENTHREAD_CONFIG_JOINER_ENABLE
@@ -533,12 +546,10 @@
     Srp::Server mSrpServer;
 #endif
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
 #if OPENTHREAD_FTD
-    Utils::ChildSupervisor mChildSupervisor;
+    ChildSupervisor mChildSupervisor;
 #endif
-    Utils::SupervisionListener mSupervisionListener;
-#endif
+    SupervisionListener mSupervisionListener;
 
     AnnounceBeginServer mAnnounceBegin;
     PanIdQueryServer    mPanIdQuery;
@@ -552,8 +563,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
@@ -576,6 +591,10 @@
     Utils::ChannelManager mChannelManager;
 #endif
 
+#if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
+    Utils::MeshDiag mMeshDiag;
+#endif
+
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
     Utils::HistoryTracker mHistoryTracker;
 #endif
@@ -596,6 +615,10 @@
     BorderRouter::RoutingManager mRoutingManager;
 #endif
 
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    Nat64::Translator mNat64Translator;
+#endif
+
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
 
 #if OPENTHREAD_RADIO || OPENTHREAD_CONFIG_LINK_RAW_ENABLE
@@ -613,6 +636,9 @@
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
     FactoryDiags::Diags mDiags;
 #endif
+#if OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+    Utils::PowerCalibration mPowerCalibration;
+#endif
 
     bool mIsInitialized;
 
@@ -626,492 +652,259 @@
 
 // Specializations of the `Get<Type>()` method.
 
-template <> inline Instance &Instance::Get(void)
-{
-    return *this;
-}
+template <> inline Instance &Instance::Get(void) { return *this; }
 
-template <> inline Radio &Instance::Get(void)
-{
-    return mRadio;
-}
+template <> inline Radio &Instance::Get(void) { return mRadio; }
 
-template <> inline Radio::Callbacks &Instance::Get(void)
-{
-    return mRadio.mCallbacks;
-}
+template <> inline Radio::Callbacks &Instance::Get(void) { return mRadio.mCallbacks; }
 
 #if OPENTHREAD_CONFIG_UPTIME_ENABLE
-template <> inline Uptime &Instance::Get(void)
-{
-    return mUptime;
-}
+template <> inline Uptime &Instance::Get(void) { return mUptime; }
 #endif
 
 #if OPENTHREAD_MTD || OPENTHREAD_FTD
-template <> inline Notifier &Instance::Get(void)
-{
-    return mNotifier;
-}
+template <> inline Notifier &Instance::Get(void) { return mNotifier; }
 
-template <> inline TimeTicker &Instance::Get(void)
-{
-    return mTimeTicker;
-}
+template <> inline TimeTicker &Instance::Get(void) { return mTimeTicker; }
 
-template <> inline Settings &Instance::Get(void)
-{
-    return mSettings;
-}
+template <> inline Settings &Instance::Get(void) { return mSettings; }
 
-template <> inline SettingsDriver &Instance::Get(void)
-{
-    return mSettingsDriver;
-}
+template <> inline SettingsDriver &Instance::Get(void) { return mSettingsDriver; }
 
-template <> inline MeshForwarder &Instance::Get(void)
-{
-    return mMeshForwarder;
-}
+template <> inline MeshForwarder &Instance::Get(void) { return mMeshForwarder; }
 
 #if OPENTHREAD_CONFIG_MULTI_RADIO
-template <> inline RadioSelector &Instance::Get(void)
-{
-    return mRadioSelector;
-}
+template <> inline RadioSelector &Instance::Get(void) { return mRadioSelector; }
 #endif
 
-template <> inline Mle::Mle &Instance::Get(void)
-{
-    return mMleRouter;
-}
+template <> inline Mle::Mle &Instance::Get(void) { return mMleRouter; }
 
-template <> inline Mle::MleRouter &Instance::Get(void)
-{
-    return mMleRouter;
-}
+template <> inline Mle::MleRouter &Instance::Get(void) { return mMleRouter; }
 
-template <> inline Mle::DiscoverScanner &Instance::Get(void)
-{
-    return mDiscoverScanner;
-}
+template <> inline Mle::DiscoverScanner &Instance::Get(void) { return mDiscoverScanner; }
 
-template <> inline NeighborTable &Instance::Get(void)
-{
-    return mMleRouter.mNeighborTable;
-}
+template <> inline NeighborTable &Instance::Get(void) { return mMleRouter.mNeighborTable; }
 
 #if OPENTHREAD_FTD
-template <> inline ChildTable &Instance::Get(void)
-{
-    return mMleRouter.mChildTable;
-}
+template <> inline ChildTable &Instance::Get(void) { return mMleRouter.mChildTable; }
 
-template <> inline RouterTable &Instance::Get(void)
-{
-    return mMleRouter.mRouterTable;
-}
+template <> inline RouterTable &Instance::Get(void) { return mMleRouter.mRouterTable; }
 #endif
 
-template <> inline Ip6::Netif &Instance::Get(void)
-{
-    return mThreadNetif;
-}
+template <> inline Ip6::Netif &Instance::Get(void) { return mThreadNetif; }
 
-template <> inline ThreadNetif &Instance::Get(void)
-{
-    return mThreadNetif;
-}
+template <> inline ThreadNetif &Instance::Get(void) { return mThreadNetif; }
 
-template <> inline Ip6::Ip6 &Instance::Get(void)
-{
-    return mIp6;
-}
+template <> inline Ip6::Ip6 &Instance::Get(void) { return mIp6; }
 
-template <> inline Mac::Mac &Instance::Get(void)
-{
-    return mMac;
-}
+template <> inline Mac::Mac &Instance::Get(void) { return mMac; }
 
-template <> inline Mac::SubMac &Instance::Get(void)
-{
-    return mMac.mLinks.mSubMac;
-}
+template <> inline Mac::SubMac &Instance::Get(void) { return mMac.mLinks.mSubMac; }
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
-template <> inline Trel::Link &Instance::Get(void)
-{
-    return mMac.mLinks.mTrel;
-}
+template <> inline Trel::Link &Instance::Get(void) { return mMac.mLinks.mTrel; }
 
-template <> inline Trel::Interface &Instance::Get(void)
-{
-    return mMac.mLinks.mTrel.mInterface;
-}
+template <> inline Trel::Interface &Instance::Get(void) { return mMac.mLinks.mTrel.mInterface; }
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
-template <> inline Mac::Filter &Instance::Get(void)
-{
-    return mMac.mFilter;
-}
+template <> inline Mac::Filter &Instance::Get(void) { return mMac.mFilter; }
 #endif
 
-template <> inline Lowpan::Lowpan &Instance::Get(void)
-{
-    return mLowpan;
-}
+template <> inline Lowpan::Lowpan &Instance::Get(void) { return mLowpan; }
 
-template <> inline KeyManager &Instance::Get(void)
-{
-    return mKeyManager;
-}
+template <> inline KeyManager &Instance::Get(void) { return mKeyManager; }
 
-template <> inline Ip6::Filter &Instance::Get(void)
-{
-    return mIp6Filter;
-}
+template <> inline Ip6::Filter &Instance::Get(void) { return mIp6Filter; }
 
-template <> inline AddressResolver &Instance::Get(void)
-{
-    return mAddressResolver;
-}
+template <> inline AddressResolver &Instance::Get(void) { return mAddressResolver; }
 
 #if OPENTHREAD_FTD
 
-template <> inline IndirectSender &Instance::Get(void)
-{
-    return mMeshForwarder.mIndirectSender;
-}
+template <> inline IndirectSender &Instance::Get(void) { return mMeshForwarder.mIndirectSender; }
 
 template <> inline SourceMatchController &Instance::Get(void)
 {
     return mMeshForwarder.mIndirectSender.mSourceMatchController;
 }
 
-template <> inline DataPollHandler &Instance::Get(void)
-{
-    return mMeshForwarder.mIndirectSender.mDataPollHandler;
-}
+template <> inline DataPollHandler &Instance::Get(void) { return mMeshForwarder.mIndirectSender.mDataPollHandler; }
 
 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
-template <> inline CslTxScheduler &Instance::Get(void)
-{
-    return mMeshForwarder.mIndirectSender.mCslTxScheduler;
-}
+template <> inline CslTxScheduler &Instance::Get(void) { return mMeshForwarder.mIndirectSender.mCslTxScheduler; }
 #endif
 
-template <> inline MeshCoP::Leader &Instance::Get(void)
-{
-    return mLeader;
-}
+template <> inline MeshCoP::Leader &Instance::Get(void) { return mLeader; }
 
-template <> inline MeshCoP::JoinerRouter &Instance::Get(void)
-{
-    return mJoinerRouter;
-}
+template <> inline MeshCoP::JoinerRouter &Instance::Get(void) { return mJoinerRouter; }
 #endif // OPENTHREAD_FTD
 
-template <> inline AnnounceBeginServer &Instance::Get(void)
-{
-    return mAnnounceBegin;
-}
+template <> inline AnnounceBeginServer &Instance::Get(void) { return mAnnounceBegin; }
 
-template <> inline DataPollSender &Instance::Get(void)
-{
-    return mMeshForwarder.mDataPollSender;
-}
+template <> inline DataPollSender &Instance::Get(void) { return mMeshForwarder.mDataPollSender; }
 
-template <> inline EnergyScanServer &Instance::Get(void)
-{
-    return mEnergyScan;
-}
+template <> inline EnergyScanServer &Instance::Get(void) { return mEnergyScan; }
 
-template <> inline PanIdQueryServer &Instance::Get(void)
-{
-    return mPanIdQuery;
-}
+template <> inline PanIdQueryServer &Instance::Get(void) { return mPanIdQuery; }
 
 #if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE
-template <> inline AnycastLocator &Instance::Get(void)
-{
-    return mAnycastLocator;
-}
+template <> inline AnycastLocator &Instance::Get(void) { return mAnycastLocator; }
 #endif
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE || OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
-template <> inline NetworkData::Local &Instance::Get(void)
-{
-    return mNetworkDataLocal;
-}
+template <> inline NetworkData::Local &Instance::Get(void) { return mNetworkDataLocal; }
 #endif
 
-template <> inline NetworkData::Leader &Instance::Get(void)
-{
-    return mNetworkDataLeader;
-}
+template <> inline NetworkData::Leader &Instance::Get(void) { return mNetworkDataLeader; }
 
 #if OPENTHREAD_FTD || OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE || OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
-template <> inline NetworkData::Notifier &Instance::Get(void)
-{
-    return mNetworkDataNotifier;
-}
+template <> inline NetworkData::Notifier &Instance::Get(void) { return mNetworkDataNotifier; }
 #endif
 
 #if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE
-template <> inline NetworkData::Publisher &Instance::Get(void)
-{
-    return mNetworkDataPublisher;
-}
+template <> inline NetworkData::Publisher &Instance::Get(void) { return mNetworkDataPublisher; }
 #endif
 
-template <> inline NetworkData::Service::Manager &Instance::Get(void)
-{
-    return mNetworkDataServiceManager;
-}
+template <> inline NetworkData::Service::Manager &Instance::Get(void) { return mNetworkDataServiceManager; }
 
 #if OPENTHREAD_CONFIG_TCP_ENABLE
-template <> inline Ip6::Tcp &Instance::Get(void)
-{
-    return mIp6.mTcp;
-}
+template <> inline Ip6::Tcp &Instance::Get(void) { return mIp6.mTcp; }
 #endif
 
-template <> inline Ip6::Udp &Instance::Get(void)
-{
-    return mIp6.mUdp;
-}
+template <> inline Ip6::Udp &Instance::Get(void) { return mIp6.mUdp; }
 
-template <> inline Ip6::Icmp &Instance::Get(void)
-{
-    return mIp6.mIcmp;
-}
+template <> inline Ip6::Icmp &Instance::Get(void) { return mIp6.mIcmp; }
 
-template <> inline Ip6::Mpl &Instance::Get(void)
-{
-    return mIp6.mMpl;
-}
+template <> inline Ip6::Mpl &Instance::Get(void) { return mIp6.mMpl; }
 
-template <> inline Tmf::Agent &Instance::Get(void)
-{
-    return mTmfAgent;
-}
+template <> inline Tmf::Agent &Instance::Get(void) { return mTmfAgent; }
 
 #if OPENTHREAD_CONFIG_DTLS_ENABLE
-template <> inline Coap::CoapSecure &Instance::Get(void)
-{
-    return mCoapSecure;
-}
+template <> inline Tmf::SecureAgent &Instance::Get(void) { return mTmfSecureAgent; }
 #endif
 
-template <> inline MeshCoP::ExtendedPanIdManager &Instance::Get(void)
-{
-    return mExtendedPanIdManager;
-}
+template <> inline MeshCoP::ExtendedPanIdManager &Instance::Get(void) { return mExtendedPanIdManager; }
 
-template <> inline MeshCoP::NetworkNameManager &Instance::Get(void)
-{
-    return mNetworkNameManager;
-}
+template <> inline MeshCoP::NetworkNameManager &Instance::Get(void) { return mNetworkNameManager; }
 
-template <> inline MeshCoP::ActiveDatasetManager &Instance::Get(void)
-{
-    return mActiveDataset;
-}
+template <> inline MeshCoP::ActiveDatasetManager &Instance::Get(void) { return mActiveDataset; }
 
-template <> inline MeshCoP::PendingDatasetManager &Instance::Get(void)
-{
-    return mPendingDataset;
-}
+template <> inline MeshCoP::PendingDatasetManager &Instance::Get(void) { return mPendingDataset; }
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-template <> inline TimeSync &Instance::Get(void)
-{
-    return mTimeSync;
-}
+template <> inline TimeSync &Instance::Get(void) { return mTimeSync; }
 #endif
 
 #if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD
-template <> inline MeshCoP::Commissioner &Instance::Get(void)
-{
-    return mCommissioner;
-}
+template <> inline MeshCoP::Commissioner &Instance::Get(void) { return mCommissioner; }
+
+template <> inline AnnounceBeginClient &Instance::Get(void) { return mCommissioner.GetAnnounceBeginClient(); }
+
+template <> inline EnergyScanClient &Instance::Get(void) { return mCommissioner.GetEnergyScanClient(); }
+
+template <> inline PanIdQueryClient &Instance::Get(void) { return mCommissioner.GetPanIdQueryClient(); }
 #endif
 
 #if OPENTHREAD_CONFIG_JOINER_ENABLE
-template <> inline MeshCoP::Joiner &Instance::Get(void)
-{
-    return mJoiner;
-}
+template <> inline MeshCoP::Joiner &Instance::Get(void) { return mJoiner; }
 #endif
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
-template <> inline Dns::Client &Instance::Get(void)
-{
-    return mDnsClient;
-}
+template <> inline Dns::Client &Instance::Get(void) { return mDnsClient; }
 #endif
 
 #if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
-template <> inline Srp::Client &Instance::Get(void)
-{
-    return mSrpClient;
-}
+template <> inline Srp::Client &Instance::Get(void) { return mSrpClient; }
 #endif
 
 #if OPENTHREAD_CONFIG_SRP_CLIENT_BUFFERS_ENABLE
-template <> inline Utils::SrpClientBuffers &Instance::Get(void)
-{
-    return mSrpClientBuffers;
-}
+template <> inline Utils::SrpClientBuffers &Instance::Get(void) { return mSrpClientBuffers; }
 #endif
 
 #if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
-template <> inline Dns::ServiceDiscovery::Server &Instance::Get(void)
-{
-    return mDnssdServer;
-}
+template <> inline Dns::ServiceDiscovery::Server &Instance::Get(void) { return mDnssdServer; }
 #endif
 
 #if OPENTHREAD_CONFIG_DNS_DSO_ENABLE
-template <> inline Dns::Dso &Instance::Get(void)
-{
-    return mDnsDso;
-}
+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
-template <> inline Dhcp6::Client &Instance::Get(void)
-{
-    return mDhcp6Client;
-}
+template <> inline Dhcp6::Client &Instance::Get(void) { return mDhcp6Client; }
 #endif
 
 #if OPENTHREAD_CONFIG_DHCP6_SERVER_ENABLE
-template <> inline Dhcp6::Server &Instance::Get(void)
-{
-    return mDhcp6Server;
-}
+template <> inline Dhcp6::Server &Instance::Get(void) { return mDhcp6Server; }
 #endif
 
 #if OPENTHREAD_CONFIG_NEIGHBOR_DISCOVERY_AGENT_ENABLE
-template <> inline NeighborDiscovery::Agent &Instance::Get(void)
-{
-    return mNeighborDiscoveryAgent;
-}
+template <> inline NeighborDiscovery::Agent &Instance::Get(void) { return mNeighborDiscoveryAgent; }
 #endif
 
 #if OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE
-template <> inline Utils::Slaac &Instance::Get(void)
-{
-    return mSlaac;
-}
+template <> inline Utils::Slaac &Instance::Get(void) { return mSlaac; }
 #endif
 
 #if OPENTHREAD_CONFIG_JAM_DETECTION_ENABLE
-template <> inline Utils::JamDetector &Instance::Get(void)
-{
-    return mJamDetector;
-}
+template <> inline Utils::JamDetector &Instance::Get(void) { return mJamDetector; }
 #endif
 
 #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE
-template <> inline Sntp::Client &Instance::Get(void)
-{
-    return mSntpClient;
-}
+template <> inline Sntp::Client &Instance::Get(void) { return mSntpClient; }
 #endif
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
 #if OPENTHREAD_FTD
-template <> inline Utils::ChildSupervisor &Instance::Get(void)
-{
-    return mChildSupervisor;
-}
+template <> inline ChildSupervisor &Instance::Get(void) { return mChildSupervisor; }
 #endif
-template <> inline Utils::SupervisionListener &Instance::Get(void)
-{
-    return mSupervisionListener;
-}
-#endif
+template <> inline SupervisionListener &Instance::Get(void) { return mSupervisionListener; }
 
 #if OPENTHREAD_CONFIG_PING_SENDER_ENABLE
-template <> inline Utils::PingSender &Instance::Get(void)
-{
-    return mPingSender;
-}
+template <> inline Utils::PingSender &Instance::Get(void) { return mPingSender; }
 #endif
 
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
-template <> inline Utils::ChannelMonitor &Instance::Get(void)
-{
-    return mChannelMonitor;
-}
+template <> inline Utils::ChannelMonitor &Instance::Get(void) { return mChannelMonitor; }
 #endif
 
 #if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE && OPENTHREAD_FTD
-template <> inline Utils::ChannelManager &Instance::Get(void)
-{
-    return mChannelManager;
-}
+template <> inline Utils::ChannelManager &Instance::Get(void) { return mChannelManager; }
+#endif
+
+#if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
+template <> inline Utils::MeshDiag &Instance::Get(void) { return mMeshDiag; }
 #endif
 
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-template <> inline Utils::HistoryTracker &Instance::Get(void)
-{
-    return mHistoryTracker;
-}
+template <> inline Utils::HistoryTracker &Instance::Get(void) { return mHistoryTracker; }
 #endif
 
 #if (OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE || OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE) && OPENTHREAD_FTD
-template <> inline MeshCoP::DatasetUpdater &Instance::Get(void)
-{
-    return mDatasetUpdater;
-}
+template <> inline MeshCoP::DatasetUpdater &Instance::Get(void) { return mDatasetUpdater; }
 #endif
 
 #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
-template <> inline MeshCoP::BorderAgent &Instance::Get(void)
-{
-    return mBorderAgent;
-}
+template <> inline MeshCoP::BorderAgent &Instance::Get(void) { return mBorderAgent; }
 #endif
 
 #if OPENTHREAD_CONFIG_ANNOUNCE_SENDER_ENABLE
-template <> inline AnnounceSender &Instance::Get(void)
-{
-    return mAnnounceSender;
-}
+template <> inline AnnounceSender &Instance::Get(void) { return mAnnounceSender; }
 #endif
 
-template <> inline MessagePool &Instance::Get(void)
-{
-    return mMessagePool;
-}
+template <> inline MessagePool &Instance::Get(void) { return mMessagePool; }
 
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
 
-template <> inline BackboneRouter::Leader &Instance::Get(void)
-{
-    return mBackboneRouterLeader;
-}
+template <> inline BackboneRouter::Leader &Instance::Get(void) { return mBackboneRouterLeader; }
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
-template <> inline BackboneRouter::Local &Instance::Get(void)
-{
-    return mBackboneRouterLocal;
-}
-template <> inline BackboneRouter::Manager &Instance::Get(void)
-{
-    return mBackboneRouterManager;
-}
+template <> inline BackboneRouter::Local   &Instance::Get(void) { return mBackboneRouterLocal; }
+template <> inline BackboneRouter::Manager &Instance::Get(void) { return mBackboneRouterManager; }
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
 template <> inline BackboneRouter::MulticastListenersTable &Instance::Get(void)
@@ -1134,100 +927,70 @@
 #endif
 
 #if OPENTHREAD_CONFIG_MLR_ENABLE || (OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE)
-template <> inline MlrManager &Instance::Get(void)
-{
-    return mMlrManager;
-}
+template <> inline MlrManager &Instance::Get(void) { return mMlrManager; }
 #endif
 
 #if OPENTHREAD_CONFIG_DUA_ENABLE || (OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE)
-template <> inline DuaManager &Instance::Get(void)
-{
-    return mDuaManager;
-}
+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)
 
 #if OPENTHREAD_CONFIG_OTNS_ENABLE
-template <> inline Utils::Otns &Instance::Get(void)
-{
-    return mOtns;
-}
+template <> inline Utils::Otns &Instance::Get(void) { return mOtns; }
 #endif
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
-template <> inline BorderRouter::RoutingManager &Instance::Get(void)
-{
-    return mRoutingManager;
-}
+template <> inline BorderRouter::RoutingManager &Instance::Get(void) { return mRoutingManager; }
 
-template <> inline BorderRouter::InfraIf &Instance::Get(void)
-{
-    return mRoutingManager.mInfraIf;
-}
+template <> inline BorderRouter::InfraIf &Instance::Get(void) { return mRoutingManager.mInfraIf; }
+#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+template <> inline Nat64::Translator &Instance::Get(void) { return mNat64Translator; }
 #endif
 
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
-template <> inline Srp::Server &Instance::Get(void)
-{
-    return mSrpServer;
-}
+template <> inline Srp::Server &Instance::Get(void) { return mSrpServer; }
 #endif
 
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
 
 #if OPENTHREAD_RADIO || OPENTHREAD_CONFIG_LINK_RAW_ENABLE
-template <> inline Mac::LinkRaw &Instance::Get(void)
-{
-    return mLinkRaw;
-}
+template <> inline Mac::LinkRaw &Instance::Get(void) { return mLinkRaw; }
 
 #if OPENTHREAD_RADIO
-template <> inline Mac::SubMac &Instance::Get(void)
-{
-    return mLinkRaw.mSubMac;
-}
+template <> inline Mac::SubMac &Instance::Get(void) { return mLinkRaw.mSubMac; }
 #endif
 
 #endif // OPENTHREAD_RADIO || OPENTHREAD_CONFIG_LINK_RAW_ENABLE
 
-template <> inline Tasklet::Scheduler &Instance::Get(void)
-{
-    return mTaskletScheduler;
-}
+template <> inline Tasklet::Scheduler &Instance::Get(void) { return mTaskletScheduler; }
 
-template <> inline TimerMilli::Scheduler &Instance::Get(void)
-{
-    return mTimerMilliScheduler;
-}
+template <> inline TimerMilli::Scheduler &Instance::Get(void) { return mTimerMilliScheduler; }
 
 #if OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
-template <> inline TimerMicro::Scheduler &Instance::Get(void)
-{
-    return mTimerMicroScheduler;
-}
+template <> inline TimerMicro::Scheduler &Instance::Get(void) { return mTimerMicroScheduler; }
 #endif
 
 #if OPENTHREAD_ENABLE_VENDOR_EXTENSION
-template <> inline Extension::ExtensionBase &Instance::Get(void)
-{
-    return mExtension;
-}
+template <> inline Extension::ExtensionBase &Instance::Get(void) { return mExtension; }
 #endif
 
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
-template <> inline FactoryDiags::Diags &Instance::Get(void)
-{
-    return mDiags;
-}
+template <> inline FactoryDiags::Diags &Instance::Get(void) { return mDiags; }
+#endif
+
+#if OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+template <> inline Utils::PowerCalibration &Instance::Get(void) { return mPowerCalibration; }
 #endif
 
 /**
diff --git a/src/core/common/linked_list.hpp b/src/core/common/linked_list.hpp
index b924877..f79ac91 100644
--- a/src/core/common/linked_list.hpp
+++ b/src/core/common/linked_list.hpp
@@ -458,10 +458,10 @@
      *
      */
     template <typename Indicator>
-    const Type *FindMatching(const Type *     aBegin,
-                             const Type *     aEnd,
+    const Type *FindMatching(const Type      *aBegin,
+                             const Type      *aEnd,
                              const Indicator &aIndicator,
-                             const Type *&    aPrevEntry) const
+                             const Type     *&aPrevEntry) const
     {
         const Type *entry;
 
diff --git a/src/core/common/locator_getters.hpp b/src/core/common/locator_getters.hpp
index e1c6885..990b8c6 100644
--- a/src/core/common/locator_getters.hpp
+++ b/src/core/common/locator_getters.hpp
@@ -39,6 +39,7 @@
 
 #include "common/instance.hpp"
 #include "common/locator.hpp"
+#include "common/tasklet.hpp"
 
 namespace ot {
 
@@ -49,6 +50,26 @@
     return static_cast<const InstanceGetProvider *>(this)->GetInstance().template Get<Type>();
 }
 
+template <typename Owner, void (Owner::*HandleTaskletPtr)(void)>
+void TaskletIn<Owner, HandleTaskletPtr>::HandleTasklet(Tasklet &aTasklet)
+{
+    (aTasklet.Get<Owner>().*HandleTaskletPtr)();
+}
+
+template <typename Owner, void (Owner::*HandleTimertPtr)(void)>
+void TimerMilliIn<Owner, HandleTimertPtr>::HandleTimer(Timer &aTimer)
+{
+    (aTimer.Get<Owner>().*HandleTimertPtr)();
+}
+
+#if OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
+template <typename Owner, void (Owner::*HandleTimertPtr)(void)>
+void TimerMicroIn<Owner, HandleTimertPtr>::HandleTimer(Timer &aTimer)
+{
+    (aTimer.Get<Owner>().*HandleTimertPtr)();
+}
+#endif
+
 } // namespace ot
 
 #endif // LOCATOR_GETTERS_HPP_
diff --git a/src/core/common/log.cpp b/src/core/common/log.cpp
index a0a126a..04c74bb 100644
--- a/src/core/common/log.cpp
+++ b/src/core/common/log.cpp
@@ -39,6 +39,7 @@
 
 #include "common/code_utils.hpp"
 #include "common/instance.hpp"
+#include "common/num_utils.hpp"
 #include "common/string.hpp"
 
 /*
@@ -97,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
 
@@ -244,7 +245,7 @@
     for (uint16_t i = 0; i < aDataLength; i += kDumpBytesPerLine)
     {
         DumpLine(aModuleName, aLogLevel, static_cast<const uint8_t *>(aData) + i,
-                 OT_MIN((aDataLength - i), kDumpBytesPerLine));
+                 Min(static_cast<uint8_t>(aDataLength - i), kDumpBytesPerLine));
     }
 
     string.Clear();
diff --git a/src/core/common/log.hpp b/src/core/common/log.hpp
index 0870e06..90fec6c 100644
--- a/src/core/common/log.hpp
+++ b/src/core/common/log.hpp
@@ -107,7 +107,7 @@
  * @param[in]  ...   Arguments for the format specification.
  *
  */
-#define LogCrit(...) Logger::Log<kLogLevelCrit, kLogModuleName>(__VA_ARGS__)
+#define LogCrit(...) Logger::LogAtLevel<kLogLevelCrit>(kLogModuleName, __VA_ARGS__)
 #else
 #define LogCrit(...)
 #endif
@@ -119,7 +119,7 @@
  * @param[in]  ...   Arguments for the format specification.
  *
  */
-#define LogWarn(...) Logger::Log<kLogLevelWarn, kLogModuleName>(__VA_ARGS__)
+#define LogWarn(...) Logger::LogAtLevel<kLogLevelWarn>(kLogModuleName, __VA_ARGS__)
 #else
 #define LogWarn(...)
 #endif
@@ -131,7 +131,7 @@
  * @param[in]  ...   Arguments for the format specification.
  *
  */
-#define LogNote(...) Logger::Log<kLogLevelNote, kLogModuleName>(__VA_ARGS__)
+#define LogNote(...) Logger::LogAtLevel<kLogLevelNote>(kLogModuleName, __VA_ARGS__)
 #else
 #define LogNote(...)
 #endif
@@ -143,7 +143,7 @@
  * @param[in]  ...   Arguments for the format specification.
  *
  */
-#define LogInfo(...) Logger::Log<kLogLevelInfo, kLogModuleName>(__VA_ARGS__)
+#define LogInfo(...) Logger::LogAtLevel<kLogLevelInfo>(kLogModuleName, __VA_ARGS__)
 #else
 #define LogInfo(...)
 #endif
@@ -155,7 +155,7 @@
  * @param[in]  ...   Arguments for the format specification.
  *
  */
-#define LogDebg(...) Logger::Log<kLogLevelDebg, kLogModuleName>(__VA_ARGS__)
+#define LogDebg(...) Logger::LogAtLevel<kLogLevelDebg>(kLogModuleName, __VA_ARGS__)
 #else
 #define LogDebg(...)
 #endif
@@ -305,15 +305,13 @@
     // and instead the logging macros should be used.
 
 public:
-    template <LogLevel kLogLevel, const char *kModuleName, typename... Args>
-    static void Log(const char *aFormat, Args... aArgs)
-    {
-        LogAtLevel<kLogLevel>(kModuleName, aFormat, aArgs...);
-    }
+    static void LogInModule(const char *aModuleName, LogLevel aLogLevel, const char *aFormat, ...)
+        OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(3, 4);
 
-    static void LogInModule(const char *aModuleName, LogLevel aLogLevel, const char *aFormat, ...);
+    template <LogLevel kLogLevel>
+    static void LogAtLevel(const char *aModuleName, const char *aFormat, ...)
+        OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
 
-    template <LogLevel kLogLevel> static void LogAtLevel(const char *aModuleName, const char *aFormat, ...);
     static void LogVarArgs(const char *aModuleName, LogLevel aLogLevel, const char *aFormat, va_list aArgs);
 
 #if OPENTHREAD_CONFIG_LOG_PKT_DUMP
diff --git a/src/core/common/message.cpp b/src/core/common/message.cpp
index 4847aac..e7727e2 100644
--- a/src/core/common/message.cpp
+++ b/src/core/common/message.cpp
@@ -40,6 +40,7 @@
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
+#include "common/num_utils.hpp"
 #include "common/numeric_limits.hpp"
 #include "net/checksum.hpp"
 #include "net/ip6.hpp"
@@ -63,9 +64,8 @@
 
 MessagePool::MessagePool(Instance &aInstance)
     : InstanceLocator(aInstance)
-#if !OPENTHREAD_CONFIG_PLATFORM_MESSAGE_MANAGEMENT && !OPENTHREAD_CONFIG_MESSAGE_USE_HEAP_ENABLE
-    , mNumFreeBuffers(kNumBuffers)
-#endif
+    , mNumAllocated(0)
+    , mMaxAllocated(0)
 {
 #if OPENTHREAD_CONFIG_PLATFORM_MESSAGE_MANAGEMENT
     otPlatMessagePoolInit(&GetInstance(), kNumBuffers, sizeof(Buffer));
@@ -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);
@@ -122,9 +129,8 @@
         SuccessOrExit(ReclaimBuffers(aPriority));
     }
 
-#if !OPENTHREAD_CONFIG_PLATFORM_MESSAGE_MANAGEMENT && !OPENTHREAD_CONFIG_MESSAGE_USE_HEAP_ENABLE
-    mNumFreeBuffers--;
-#endif
+    mNumAllocated++;
+    mMaxAllocated = Max(mMaxAllocated, mNumAllocated);
 
     buffer->SetNextBuffer(nullptr);
 
@@ -148,16 +154,14 @@
         otPlatMessagePoolFree(&GetInstance(), aBuffer);
 #else
         mBufferPool.Free(*aBuffer);
-        mNumFreeBuffers++;
 #endif
+        mNumAllocated--;
+
         aBuffer = next;
     }
 }
 
-Error MessagePool::ReclaimBuffers(Message::Priority aPriority)
-{
-    return Get<MeshForwarder>().EvictMessage(aPriority);
-}
+Error MessagePool::ReclaimBuffers(Message::Priority aPriority) { return Get<MeshForwarder>().EvictMessage(aPriority); }
 
 uint16_t MessagePool::GetFreeBufferCount(void) const
 {
@@ -172,7 +176,7 @@
 #elif OPENTHREAD_CONFIG_PLATFORM_MESSAGE_MANAGEMENT
     rval = otPlatMessagePoolNumFreeBuffers(&GetInstance());
 #else
-    rval = mNumFreeBuffers;
+    rval = kNumBuffers - mNumAllocated;
 #endif
 
     return rval;
@@ -229,8 +233,8 @@
     // requested length.
 
     Error    error     = kErrorNone;
-    Buffer * curBuffer = this;
-    Buffer * lastBuffer;
+    Buffer  *curBuffer = this;
+    Buffer  *lastBuffer;
     uint16_t curLength = kHeadBufferDataSize;
 
     while (curLength < aLength)
@@ -255,10 +259,7 @@
     return error;
 }
 
-void Message::Free(void)
-{
-    GetMessagePool()->Free(this);
-}
+void Message::Free(void) { GetMessagePool()->Free(this); }
 
 Message *Message::GetNext(void) const
 {
@@ -487,6 +488,67 @@
     }
 }
 
+void Message::RemoveHeader(uint16_t aOffset, uint16_t aLength)
+{
+    // To shrink the header, we copy the header byte before `aOffset`
+    // forward. Starting at offset `aLength`, we write bytes we read
+    // from offset `0` onward and copy a total of `aOffset` bytes.
+    // Then remove the first `aLength` bytes from message.
+    //
+    //
+    // 0                   aOffset  aOffset + aLength
+    // +-----------------------+---------+------------------------+
+    // | / / / / / / / / / / / | x x x x |                        |
+    // +-----------------------+---------+------------------------+
+    //
+    // 0       aLength                aOffset + aLength
+    // +---------+-----------------------+------------------------+
+    // |         | / / / / / / / / / / / |                        |
+    // +---------+-----------------------+------------------------+
+    //
+    //  0                    aOffset
+    //  +-----------------------+------------------------+
+    //  | / / / / / / / / / / / |                        |
+    //  +-----------------------+------------------------+
+    //
+
+    WriteBytesFromMessage(/* aWriteOffset */ aLength, *this, /* aReadOffset */ 0, /* aLength */ aOffset);
+    RemoveHeader(aLength);
+}
+
+Error Message::InsertHeader(uint16_t aOffset, uint16_t aLength)
+{
+    Error error;
+
+    // To make space in header at `aOffset`, we first prepend
+    // `aLength` bytes at front. Then copy the existing bytes
+    // backwards. Starting at offset `0`, we write bytes we read
+    // from offset `aLength` onward and copy a total of `aOffset`
+    // bytes.
+    //
+    // 0                    aOffset
+    // +-----------------------+------------------------+
+    // | / / / / / / / / / / / |                        |
+    // +-----------------------+------------------------+
+    //
+    // 0       aLength                aOffset + aLength
+    // +---------+-----------------------+------------------------+
+    // |         | / / / / / / / / / / / |                        |
+    // +---------+-----------------------+------------------------+
+    //
+    // 0                   aOffset  aOffset + aLength
+    // +-----------------------+---------+------------------------+
+    // | / / / / / / / / / / / |  N E W  |                        |
+    // +-----------------------+---------+------------------------+
+    //
+
+    SuccessOrExit(error = PrependBytes(nullptr, aLength));
+    WriteBytesFromMessage(/* aWriteOffset */ 0, *this, /* aReadOffset */ aLength, /* aLength */ aOffset);
+
+exit:
+    return error;
+}
+
 void Message::GetFirstChunk(uint16_t aOffset, uint16_t &aLength, Chunk &aChunk) const
 {
     // This method gets the first message chunk (contiguous data
@@ -652,29 +714,49 @@
     }
 }
 
-uint16_t Message::CopyTo(uint16_t aSourceOffset, uint16_t aDestinationOffset, uint16_t aLength, Message &aMessage) const
+void Message::WriteBytesFromMessage(uint16_t       aWriteOffset,
+                                    const Message &aMessage,
+                                    uint16_t       aReadOffset,
+                                    uint16_t       aLength)
 {
-    uint16_t bytesCopied = 0;
-    Chunk    chunk;
-
-    // This implementing can potentially overwrite the data when bytes are
-    // being copied forward within the same message, i.e., source and
-    // destination messages are the same, and source offset is smaller than
-    // the destination offset. We assert not allowing such a use.
-
-    OT_ASSERT((&aMessage != this) || (aSourceOffset >= aDestinationOffset));
-
-    GetFirstChunk(aSourceOffset, aLength, chunk);
-
-    while (chunk.GetLength() > 0)
+    if ((&aMessage != this) || (aReadOffset >= aWriteOffset))
     {
-        aMessage.WriteBytes(aDestinationOffset, chunk.GetBytes(), chunk.GetLength());
-        aDestinationOffset += chunk.GetLength();
-        bytesCopied += chunk.GetLength();
-        GetNextChunk(aLength, chunk);
-    }
+        Chunk chunk;
 
-    return bytesCopied;
+        aMessage.GetFirstChunk(aReadOffset, aLength, chunk);
+
+        while (chunk.GetLength() > 0)
+        {
+            WriteBytes(aWriteOffset, chunk.GetBytes(), chunk.GetLength());
+            aWriteOffset += chunk.GetLength();
+            aMessage.GetNextChunk(aLength, chunk);
+        }
+    }
+    else
+    {
+        // We are copying bytes within the same message forward.
+        // To ensure copy forward works, we read and write from
+        // end of range and move backwards.
+
+        static constexpr uint16_t kBufSize = 32;
+
+        uint8_t buf[kBufSize];
+
+        aWriteOffset += aLength;
+        aReadOffset += aLength;
+
+        while (aLength > 0)
+        {
+            uint16_t copyLength = Min(kBufSize, aLength);
+
+            aLength -= copyLength;
+            aReadOffset -= copyLength;
+            aWriteOffset -= copyLength;
+
+            ReadBytes(aReadOffset, buf, copyLength);
+            WriteBytes(aWriteOffset, buf, copyLength);
+        }
+    }
 }
 
 Message *Message::Clone(uint16_t aLength) const
@@ -684,13 +766,13 @@
     Settings settings(IsLinkSecurityEnabled() ? kWithLinkSecurity : kNoLinkSecurity, GetPriority());
     uint16_t offset;
 
+    aLength     = Min(GetLength(), aLength);
     messageCopy = GetMessagePool()->Allocate(GetType(), GetReserved(), settings);
     VerifyOrExit(messageCopy != nullptr, error = kErrorNoBufs);
-    SuccessOrExit(error = messageCopy->SetLength(aLength));
-    CopyTo(0, 0, aLength, *messageCopy);
+    SuccessOrExit(error = messageCopy->AppendBytesFromMessage(*this, 0, aLength));
 
     // Copy selected message information.
-    offset = GetOffset() < aLength ? GetOffset() : aLength;
+    offset = Min(GetOffset(), aLength);
     messageCopy->SetOffset(offset);
 
     messageCopy->SetSubType(GetSubType());
@@ -703,25 +785,13 @@
     return messageCopy;
 }
 
-bool Message::GetChildMask(uint16_t aChildIndex) const
-{
-    return GetMetadata().mChildMask.Get(aChildIndex);
-}
+bool Message::GetChildMask(uint16_t aChildIndex) const { return GetMetadata().mChildMask.Get(aChildIndex); }
 
-void Message::ClearChildMask(uint16_t aChildIndex)
-{
-    GetMetadata().mChildMask.Set(aChildIndex, false);
-}
+void Message::ClearChildMask(uint16_t aChildIndex) { GetMetadata().mChildMask.Set(aChildIndex, false); }
 
-void Message::SetChildMask(uint16_t aChildIndex)
-{
-    GetMetadata().mChildMask.Set(aChildIndex, true);
-}
+void Message::SetChildMask(uint16_t aChildIndex) { GetMetadata().mChildMask.Set(aChildIndex, true); }
 
-bool Message::IsChildPending(void) const
-{
-    return GetMetadata().mChildMask.HasAny();
-}
+bool Message::IsChildPending(void) const { return GetMetadata().mChildMask.HasAny(); }
 
 void Message::SetLinkInfo(const ThreadLinkInfo &aLinkInfo)
 {
@@ -835,15 +905,9 @@
     }
 }
 
-Message::Iterator MessageQueue::begin(void)
-{
-    return Message::Iterator(GetHead());
-}
+Message::Iterator MessageQueue::begin(void) { return Message::Iterator(GetHead()); }
 
-Message::ConstIterator MessageQueue::begin(void) const
-{
-    return Message::ConstIterator(GetHead());
-}
+Message::ConstIterator MessageQueue::begin(void) const { return Message::ConstIterator(GetHead()); }
 
 void MessageQueue::GetInfo(Info &aInfo) const
 {
@@ -909,16 +973,13 @@
     return head;
 }
 
-const Message *PriorityQueue::GetTail(void) const
-{
-    return FindFirstNonNullTail(Message::kPriorityLow);
-}
+const Message *PriorityQueue::GetTail(void) const { return FindFirstNonNullTail(Message::kPriorityLow); }
 
 void PriorityQueue::Enqueue(Message &aMessage)
 {
     Message::Priority priority;
-    Message *         tail;
-    Message *         next;
+    Message          *tail;
+    Message          *next;
 
     OT_ASSERT(!aMessage.IsInAQueue());
 
@@ -949,7 +1010,7 @@
 void PriorityQueue::Dequeue(Message &aMessage)
 {
     Message::Priority priority;
-    Message *         tail;
+    Message          *tail;
 
     OT_ASSERT(aMessage.GetPriorityQueue() == this);
 
@@ -993,15 +1054,9 @@
     }
 }
 
-Message::Iterator PriorityQueue::begin(void)
-{
-    return Message::Iterator(GetHead());
-}
+Message::Iterator PriorityQueue::begin(void) { return Message::Iterator(GetHead()); }
 
-Message::ConstIterator PriorityQueue::begin(void) const
-{
-    return Message::ConstIterator(GetHead());
-}
+Message::ConstIterator PriorityQueue::begin(void) const { return Message::ConstIterator(GetHead()); }
 
 void PriorityQueue::GetInfo(Info &aInfo) const
 {
diff --git a/src/core/common/message.hpp b/src/core/common/message.hpp
index 78adcee..3073837 100644
--- a/src/core/common/message.hpp
+++ b/src/core/common/message.hpp
@@ -39,6 +39,7 @@
 #include <stdint.h>
 
 #include <openthread/message.h>
+#include <openthread/nat64.h>
 #include <openthread/platform/messagepool.h>
 
 #include "common/as_core_type.hpp"
@@ -186,10 +187,10 @@
 protected:
     struct Metadata
     {
-        Message *    mNext;        // Next message in a doubly linked list.
-        Message *    mPrev;        // Previous message in a doubly linked list.
+        Message     *mNext;        // Next message in a doubly linked list.
+        Message     *mPrev;        // Previous message in a doubly linked list.
         MessagePool *mMessagePool; // Message pool for this message.
-        void *       mQueue;       // The queue where message is queued (if any). Queue type from `mInPriorityQ`.
+        void        *mQueue;       // The queue where message is queued (if any). Queue type from `mInPriorityQ`.
         uint32_t     mDatagramTag; // The datagram tag used for 6LoWPAN frags or IPv6fragmentation.
         TimeMilli    mTimestamp;   // The message timestamp.
         uint16_t     mReserved;    // Number of reserved bytes (for header).
@@ -231,13 +232,13 @@
     static constexpr uint16_t kBufferDataSize     = kBufferSize - sizeof(otMessageBuffer);
     static constexpr uint16_t kHeadBufferDataSize = kBufferDataSize - sizeof(Metadata);
 
-    Metadata &      GetMetadata(void) { return mBuffer.mHead.mMetadata; }
+    Metadata       &GetMetadata(void) { return mBuffer.mHead.mMetadata; }
     const Metadata &GetMetadata(void) const { return mBuffer.mHead.mMetadata; }
 
-    uint8_t *      GetFirstData(void) { return mBuffer.mHead.mData; }
+    uint8_t       *GetFirstData(void) { return mBuffer.mHead.mData; }
     const uint8_t *GetFirstData(void) const { return mBuffer.mHead.mData; }
 
-    uint8_t *      GetData(void) { return mBuffer.mData; }
+    uint8_t       *GetData(void) { return mBuffer.mData; }
     const uint8_t *GetData(void) const { return mBuffer.mData; }
 
 private:
@@ -252,7 +253,8 @@
     } mBuffer;
 };
 
-static_assert(sizeof(Buffer) >= kBufferSize, "Buffer size if not valid");
+static_assert(sizeof(Buffer) >= kBufferSize,
+              "Buffer size is not valid. Increase OPENTHREAD_CONFIG_MESSAGE_BUFFER_SIZE.");
 
 /**
  * This class represents a message.
@@ -279,7 +281,8 @@
         kType6lowpan      = 1, ///< A 6lowpan frame
         kTypeSupervision  = 2, ///< A child supervision frame.
         kTypeMacEmptyData = 3, ///< An empty MAC data frame.
-        kTypeOther        = 4, ///< Other (data) message.
+        kTypeIp4          = 4, ///< A full uncompressed IPv4 packet, for NAT64.
+        kTypeOther        = 5, ///< Other (data) message.
     };
 
     /**
@@ -605,14 +608,49 @@
     }
 
     /**
-     * This method removes header bytes from the message.
+     * This method removes header bytes from the message at start of message.
      *
-     * @param[in]  aLength  Number of header bytes to remove.
+     * The caller MUST ensure that message contains the bytes to be removed, i.e. `aOffset` is smaller than the message
+     * length.
+     *
+     * @param[in]  aLength  Number of header bytes to remove from start of `Message`.
      *
      */
     void RemoveHeader(uint16_t aLength);
 
     /**
+     * This method removes header bytes from the message at a given offset.
+     *
+     * This method shrinks the message. The existing header bytes before @p aOffset are copied forward and replace the
+     * removed bytes.
+     *
+     * The caller MUST ensure that message contains the bytes to be removed, i.e. `aOffset + aLength` is smaller than
+     * the message length.
+     *
+     * @param[in]  aOffset  The offset to start removing.
+     * @param[in]  aLength  Number of header bytes to remove.
+     *
+     */
+    void RemoveHeader(uint16_t aOffset, uint16_t aLength);
+
+    /**
+     * This method grows the message to make space for new header bytes at a given offset.
+     *
+     * This method grows the message header (similar to `PrependBytes()`). The existing header bytes from start to
+     * `aOffset + aLength` are then copied backward to make room for the new header bytes. Note that this method does
+     * not change the bytes from @p aOffset up @p aLength (the new inserted header range). Caller can write to this
+     * range to update the bytes after successful return from this method.
+     *
+     * @param[in] aOffset   The offset at which to insert the header bytes
+     * @param[in] aLength   Number of header bytes to insert.
+     *
+     * @retval kErrorNone    Successfully grown the message and copied the existing header bytes.
+     * @retval kErrorNoBufs  Insufficient available buffers to grow the message.
+     *
+     */
+    Error InsertHeader(uint16_t aOffset, uint16_t aLength);
+
+    /**
      * This method appends bytes to the end of the message.
      *
      * On success, this method grows the message by @p aLength bytes.
@@ -807,6 +845,23 @@
     void WriteBytes(uint16_t aOffset, const void *aBuf, uint16_t aLength);
 
     /**
+     * This method writes bytes read from another or potentially the same message to the message at a given offset.
+     *
+     * This method will not resize the message. The bytes to write (with @p aLength) MUST fit within the existing
+     * message buffer (from the given @p aWriteOffset up to the message's length).
+     *
+     * This method can be used to copy bytes within the same message in either direction, i.e., copy forward where
+     * `aWriteOffset > aReadOffset` or copy backward where `aWriteOffset < aReadOffset`.
+     *
+     * @param[in] aWriteOffset  Byte offset within this message to begin writing.
+     * @param[in] aMessage      The message to read the bytes from.
+     * @param[in] aReadOffset   The offset in @p aMessage to start reading the bytes from.
+     * @param[in] aLength       The number of bytes to read from @p aMessage and write.
+     *
+     */
+    void WriteBytesFromMessage(uint16_t aWriteOffset, const Message &aMessage, uint16_t aReadOffset, uint16_t aLength);
+
+    /**
      * This methods writes an object to the message.
      *
      * This method will not resize the message. The entire given object (all its bytes) MUST fit within the existing
@@ -843,23 +898,6 @@
     }
 
     /**
-     * This method copies bytes from one message to another.
-     *
-     * If source and destination messages are the same, `CopyTo()` can be used to perform a backward copy, but
-     * it MUST not be used to forward copy within the same message (i.e., when source and destination messages are the
-     * same and source offset is smaller than the destination offset).
-     *
-     * @param[in] aSourceOffset       Byte offset within the source message to begin reading.
-     * @param[in] aDestinationOffset  Byte offset within the destination message to begin writing.
-     * @param[in] aLength             Number of bytes to copy.
-     * @param[in] aMessage            Message to copy to.
-     *
-     * @returns The number of bytes copied.
-     *
-     */
-    uint16_t CopyTo(uint16_t aSourceOffset, uint16_t aDestinationOffset, uint16_t aLength, Message &aMessage) const;
-
-    /**
      * This method creates a copy of the message.
      *
      * It allocates the new message from the same message pool as the original one and copies @p aLength octets
@@ -1123,7 +1161,7 @@
     /**
      * This method returns the average RSS (Received Signal Strength) associated with the message.
      *
-     * @returns The current average RSS value (in dBm) or OT_RADIO_RSSI_INVALID if no average is available.
+     * @returns The current average RSS value (in dBm) or `Radio::kInvalidRssi` if no average is available.
      *
      */
     int8_t GetAverageRss(void) const { return GetMetadata().mRssAverager.GetAverage(); }
@@ -1370,11 +1408,11 @@
     void SetMessageQueue(MessageQueue *aMessageQueue);
     void SetPriorityQueue(PriorityQueue *aPriorityQueue);
 
-    Message *&      Next(void) { return GetMetadata().mNext; }
+    Message       *&Next(void) { return GetMetadata().mNext; }
     Message *const &Next(void) const { return GetMetadata().mNext; }
-    Message *&      Prev(void) { return GetMetadata().mPrev; }
+    Message       *&Prev(void) { return GetMetadata().mPrev; }
 
-    static Message *      NextOf(Message *aMessage) { return (aMessage != nullptr) ? aMessage->Next() : nullptr; }
+    static Message       *NextOf(Message *aMessage) { return (aMessage != nullptr) ? aMessage->Next() : nullptr; }
     static const Message *NextOf(const Message *aMessage) { return (aMessage != nullptr) ? aMessage->Next() : nullptr; }
 
     Error ResizeMessage(uint16_t aLength);
@@ -1489,7 +1527,7 @@
     Message::ConstIterator end(void) const { return Message::ConstIterator(); }
 
 private:
-    Message *      GetTail(void) { return static_cast<Message *>(mData); }
+    Message       *GetTail(void) { return static_cast<Message *>(mData); }
     const Message *GetTail(void) const { return static_cast<const Message *>(mData); }
     void           SetTail(Message *aMessage) { mData = aMessage; }
 };
@@ -1503,6 +1541,7 @@
     friend class Message;
     friend class MessageQueue;
     friend class MessagePool;
+    friend class Clearable<PriorityQueue>;
 
 public:
     typedef otMessageQueueInfo Info; ///< This struct represents info (number of messages/buffers) about a queue.
@@ -1667,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.
@@ -1695,21 +1753,36 @@
      */
     uint16_t GetTotalBufferCount(void) const;
 
+    /**
+     * This method returns the maximum number of buffers in use at the same time since OT stack initialization or
+     * since last call to `ResetMaxUsedBufferCount()`.
+     *
+     * @returns The maximum number of buffers in use at the same time so far (buffer allocation watermark).
+     *
+     */
+    uint16_t GetMaxUsedBufferCount(void) const { return mMaxAllocated; }
+
+    /**
+     * This method resets the tracked maximum number of buffers in use.
+     *
+     * @sa GetMaxUsedBufferCount
+     *
+     */
+    void ResetMaxUsedBufferCount(void) { mMaxAllocated = mNumAllocated; }
+
 private:
     Buffer *NewBuffer(Message::Priority aPriority);
     void    FreeBuffers(Buffer *aBuffer);
     Error   ReclaimBuffers(Message::Priority aPriority);
 
 #if !OPENTHREAD_CONFIG_PLATFORM_MESSAGE_MANAGEMENT && !OPENTHREAD_CONFIG_MESSAGE_USE_HEAP_ENABLE
-    uint16_t                  mNumFreeBuffers;
     Pool<Buffer, kNumBuffers> mBufferPool;
 #endif
+    uint16_t mNumAllocated;
+    uint16_t mMaxAllocated;
 };
 
-inline Instance &Message::GetInstance(void) const
-{
-    return GetMessagePool()->GetInstance();
-}
+inline Instance &Message::GetInstance(void) const { return GetMessagePool()->GetInstance(); }
 
 /**
  * @}
diff --git a/src/core/common/new.hpp b/src/core/common/new.hpp
index ab437b1..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"
 
@@ -40,9 +40,6 @@
 
 #include <openthread/platform/toolchain.h>
 
-inline void *operator new(size_t, void *p) throw()
-{
-    return p;
-}
+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/non_copyable.hpp b/src/core/common/non_copyable.hpp
index d2bd9f9..b6e44fe 100644
--- a/src/core/common/non_copyable.hpp
+++ b/src/core/common/non_copyable.hpp
@@ -43,7 +43,7 @@
 class NonCopyable
 {
 public:
-    NonCopyable(const NonCopyable &) = delete;
+    NonCopyable(const NonCopyable &)            = delete;
     NonCopyable &operator=(const NonCopyable &) = delete;
 
 protected:
diff --git a/src/core/common/notifier.cpp b/src/core/common/notifier.cpp
index 7fd2831..4cc5a96 100644
--- a/src/core/common/notifier.cpp
+++ b/src/core/common/notifier.cpp
@@ -46,12 +46,11 @@
 
 Notifier::Notifier(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mTask(aInstance, Notifier::EmitEvents)
+    , mTask(aInstance)
 {
     for (ExternalCallback &callback : mExternalCallbacks)
     {
-        callback.mHandler = nullptr;
-        callback.mContext = nullptr;
+        callback.Clear();
     }
 }
 
@@ -64,23 +63,17 @@
 
     for (ExternalCallback &callback : mExternalCallbacks)
     {
-        if (callback.mHandler == nullptr)
+        VerifyOrExit(!callback.Matches(aCallback, aContext), error = kErrorAlready);
+
+        if (!callback.IsSet() && (unusedCallback == nullptr))
         {
-            if (unusedCallback == nullptr)
-            {
-                unusedCallback = &callback;
-            }
-
-            continue;
+            unusedCallback = &callback;
         }
-
-        VerifyOrExit((callback.mHandler != aCallback) || (callback.mContext != aContext), error = kErrorAlready);
     }
 
     VerifyOrExit(unusedCallback != nullptr, error = kErrorNoBufs);
 
-    unusedCallback->mHandler = aCallback;
-    unusedCallback->mContext = aContext;
+    unusedCallback->Set(aCallback, aContext);
 
 exit:
     return error;
@@ -92,10 +85,9 @@
 
     for (ExternalCallback &callback : mExternalCallbacks)
     {
-        if ((callback.mHandler == aCallback) && (callback.mContext == aContext))
+        if (callback.Matches(aCallback, aContext))
         {
-            callback.mHandler = nullptr;
-            callback.mContext = nullptr;
+            callback.Clear();
         }
     }
 
@@ -118,11 +110,6 @@
     }
 }
 
-void Notifier::EmitEvents(Tasklet &aTasklet)
-{
-    aTasklet.Get<Notifier>().EmitEvents();
-}
-
 void Notifier::EmitEvents(void)
 {
     Events events;
@@ -146,9 +133,7 @@
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
     Get<BackboneRouter::Manager>().HandleNotifierEvents(events);
 #endif
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-    Get<Utils::ChildSupervisor>().HandleNotifierEvents(events);
-#endif
+    Get<ChildSupervisor>().HandleNotifierEvents(events);
 #if OPENTHREAD_CONFIG_DATASET_UPDATER_ENABLE || OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE
     Get<MeshCoP::DatasetUpdater>().HandleNotifierEvents(events);
 #endif
@@ -204,10 +189,7 @@
 
     for (ExternalCallback &callback : mExternalCallbacks)
     {
-        if (callback.mHandler != nullptr)
-        {
-            callback.mHandler(events.GetAsFlags(), callback.mContext);
-        }
+        callback.InvokeIfSet(events.GetAsFlags());
     }
 
 exit:
@@ -233,7 +215,7 @@
         {
             if (string.GetLength() >= kFlagsStringLineLimit)
             {
-                LogInfo("StateChanged (0x%08x) %s%s ...", aEvents.GetAsFlags(), didLog ? "... " : "[",
+                LogInfo("StateChanged (0x%08lx) %s%s ...", ToUlong(aEvents.GetAsFlags()), didLog ? "... " : "[",
                         string.AsCString());
                 string.Clear();
                 didLog   = true;
@@ -248,7 +230,7 @@
     }
 
 exit:
-    LogInfo("StateChanged (0x%08x) %s%s]", aEvents.GetAsFlags(), didLog ? "... " : "[", string.AsCString());
+    LogInfo("StateChanged (0x%08lx) %s%s]", ToUlong(aEvents.GetAsFlags()), didLog ? "... " : "[", string.AsCString());
 }
 
 const char *Notifier::EventToString(Event aEvent) const
@@ -305,14 +287,9 @@
 
 #else // #if OT_SHOULD_LOG_AT( OT_LOG_LEVEL_INFO)
 
-void Notifier::LogEvents(Events) const
-{
-}
+void Notifier::LogEvents(Events) const {}
 
-const char *Notifier::EventToString(Event) const
-{
-    return "";
-}
+const char *Notifier::EventToString(Event) const { return ""; }
 
 #endif // #if OT_SHOULD_LOG_AT( OT_LOG_LEVEL_INFO)
 
diff --git a/src/core/common/notifier.hpp b/src/core/common/notifier.hpp
index 8887671..cd05f2d 100644
--- a/src/core/common/notifier.hpp
+++ b/src/core/common/notifier.hpp
@@ -42,6 +42,7 @@
 #include <openthread/instance.h>
 #include <openthread/platform/toolchain.h>
 
+#include "common/callback.hpp"
 #include "common/error.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
@@ -95,6 +96,7 @@
     kEventJoinerStateChanged               = OT_CHANGED_JOINER_STATE,                 ///< Joiner state changed
     kEventActiveDatasetChanged             = OT_CHANGED_ACTIVE_DATASET,               ///< Active Dataset changed
     kEventPendingDatasetChanged            = OT_CHANGED_PENDING_DATASET,              ///< Pending Dataset changed
+    kEventNat64TranslatorStateChanged      = OT_CHANGED_NAT64_TRANSLATOR_STATE,       ///< Nat64Translator state changed
 };
 
 /**
@@ -307,21 +309,18 @@
 
     static constexpr uint16_t kFlagsStringBufferSize = kFlagsStringLineLimit + kMaxFlagNameLength;
 
-    struct ExternalCallback
-    {
-        otStateChangedCallback mHandler;
-        void *                 mContext;
-    };
+    typedef Callback<otStateChangedCallback> ExternalCallback;
 
-    static void EmitEvents(Tasklet &aTasklet);
-    void        EmitEvents(void);
+    void EmitEvents(void);
 
     void        LogEvents(Events aEvents) const;
     const char *EventToString(Event aEvent) const;
 
+    using EmitEventsTask = TaskletIn<Notifier, &Notifier::EmitEvents>;
+
     Events           mEventsToSignal;
     Events           mSignaledEvents;
-    Tasklet          mTask;
+    EmitEventsTask   mTask;
     ExternalCallback mExternalCallbacks[kMaxExternalHandlers];
 };
 
diff --git a/src/core/common/num_utils.hpp b/src/core/common/num_utils.hpp
new file mode 100644
index 0000000..dff0ce1
--- /dev/null
+++ b/src/core/common/num_utils.hpp
@@ -0,0 +1,223 @@
+/*
+ *  Copyright (c) 2022, 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 definitions for generic number utility functions (min, max, clamp).
+ */
+
+#ifndef NUM_UTILS_HPP_
+#define NUM_UTILS_HPP_
+
+#include "common/numeric_limits.hpp"
+#include "common/type_traits.hpp"
+
+namespace ot {
+
+/**
+ * This template function returns the minimum of two given values.
+ *
+ * Uses `operator<` to compare the values.
+ *
+ * @tparam Type   The value type.
+ *
+ * @param[in] aFirst  The first value.
+ * @param[in] aSecond The second value.
+ *
+ * @returns The minimum of @p aFirst and @p aSecond.
+ *
+ */
+template <typename Type> Type Min(Type aFirst, Type aSecond) { return (aFirst < aSecond) ? aFirst : aSecond; }
+
+/**
+ * This template function returns the maximum of two given values.
+ *
+ * Uses `operator<` to compare the values.
+ *
+ * @tparam Type   The value type.
+ *
+ * @param[in] aFirst  The first value.
+ * @param[in] aSecond The second value.
+ *
+ * @returns The maximum of @p aFirst and @p aSecond.
+ *
+ */
+template <typename Type> Type Max(Type aFirst, Type aSecond) { return (aFirst < aSecond) ? aSecond : aFirst; }
+
+/**
+ * This template function returns clamped version of a given value to a given closed range [min, max].
+ *
+ * Uses `operator<` to compare the values. The behavior is undefined if the value of @p aMin is greater than @p aMax.
+ *
+ * @tparam Type   The value type.
+ *
+ * @param[in] aValue   The value to clamp.
+ * @param[in] aMin     The minimum value.
+ * @param[in] aMax     The maximum value.
+ *
+ * @returns The clamped version of @aValue to the closed range [@p aMin, @p aMax].
+ *
+ */
+template <typename Type> Type Clamp(Type aValue, Type aMin, Type aMax)
+{
+    Type value = Max(aValue, aMin);
+
+    return Min(value, aMax);
+}
+
+/**
+ * This template function returns a clamped version of given integer to a `uint8_t`.
+ *
+ * If @p aValue is greater than max value of a `uint8_t`, the max value is returned.
+ *
+ * @tparam UintType   The value type (MUST be `uint16_t`, `uint32_t`, or `uint64_t`).
+ *
+ * @param[in] aValue  The value to clamp.
+ *
+ * @returns The clamped version of @p aValue to `uint8_t`.
+ *
+ */
+template <typename UintType> uint8_t ClampToUint8(UintType aValue)
+{
+    static_assert(TypeTraits::IsSame<UintType, uint16_t>::kValue || TypeTraits::IsSame<UintType, uint32_t>::kValue ||
+                      TypeTraits::IsSame<UintType, uint64_t>::kValue,
+                  "UintType must be `uint16_t, `uint32_t`, or `uint64_t`");
+
+    return static_cast<uint8_t>(Min(aValue, static_cast<UintType>(NumericLimits<uint8_t>::kMax)));
+}
+
+/**
+ * This template function returns a clamped version of given integer to a `uint16_t`.
+ *
+ * If @p aValue is greater than max value of a `uint16_t`, the max value is returned.
+ *
+ * @tparam UintType   The value type (MUST be `uint32_t`, or `uint64_t`).
+ *
+ * @param[in] aValue  The value to clamp.
+ *
+ * @returns The clamped version of @p aValue to `uint16_t`.
+ *
+ */
+template <typename UintType> uint16_t ClampToUint16(UintType aValue)
+{
+    static_assert(TypeTraits::IsSame<UintType, uint32_t>::kValue || TypeTraits::IsSame<UintType, uint64_t>::kValue,
+                  "UintType must be `uint32_t` or `uint64_t`");
+
+    return static_cast<uint16_t>(Min(aValue, static_cast<UintType>(NumericLimits<uint16_t>::kMax)));
+}
+
+/**
+ * This template function performs a three-way comparison between two values.
+ *
+ * @tparam Type   The value type.
+ *
+ * @param[in] aFirst  The first value.
+ * @param[in] aSecond The second value.
+ *
+ * @retval 1    If @p aFirst >  @p aSecond.
+ * @retval 0    If @p aFirst == @p aSecond.
+ * @retval -1   If @p aFirst <  @p aSecond.
+ *
+ */
+template <typename Type> int ThreeWayCompare(Type aFirst, Type aSecond)
+{
+    return (aFirst == aSecond) ? 0 : ((aFirst > aSecond) ? 1 : -1);
+}
+
+/**
+ * This is template specialization of three-way comparison between two boolean values.
+ *
+ * @param[in] aFirst  The first boolean value.
+ * @param[in] aSecond The second boolean value.
+ *
+ * @retval 1    If @p aFirst is true and @p aSecond is false (true > false).
+ * @retval 0    If both @p aFirst and @p aSecond are true, or both are false (they are equal).
+ * @retval -1   If @p aFirst is false and @p aSecond is true (false < true).
+ *
+ */
+template <> inline int ThreeWayCompare(bool aFirst, bool aSecond)
+{
+    return (aFirst == aSecond) ? 0 : (aFirst ? 1 : -1);
+}
+
+/**
+ * This template function divides two numbers and rounds the result to the closest integer.
+ *
+ * @tparam IntType   The integer type.
+ *
+ * @param[in] aDividend   The dividend value.
+ * @param[in] aDivisor    The divisor value.
+ *
+ * @return The result of division and rounding to the closest integer.
+ *
+ */
+template <typename IntType> inline IntType DivideAndRoundToClosest(IntType aDividend, IntType aDivisor)
+{
+    return (aDividend + (aDivisor / 2)) / aDivisor;
+}
+
+/**
+ * This function casts a given `uint32_t` to `unsigned long`.
+ *
+ * @param[in] aUint32   A `uint32_t` value.
+ *
+ * @returns The @p aUint32 value as `unsigned long`.
+ *
+ */
+inline unsigned long ToUlong(uint32_t aUint32) { return static_cast<unsigned long>(aUint32); }
+
+/**
+ * This function counts the number of `1` bits in the binary representation of a given unsigned int bit-mask value.
+ *
+ * @tparam UintType   The unsigned int type (MUST be `uint8_t`, uint16_t`, uint32_t`, or `uint64_t`).
+ *
+ * @param[in] aMask   A bit mask.
+ *
+ * @returns The number of `1` bits in @p aMask.
+ *
+ */
+template <typename UintType> uint8_t CountBitsInMask(UintType aMask)
+{
+    static_assert(TypeTraits::IsSame<UintType, uint8_t>::kValue || TypeTraits::IsSame<UintType, uint16_t>::kValue ||
+                      TypeTraits::IsSame<UintType, uint32_t>::kValue || TypeTraits::IsSame<UintType, uint64_t>::kValue,
+                  "UintType must be `uint8_t`, `uint16_t`, `uint32_t`, or `uint64_t`");
+
+    uint8_t count = 0;
+
+    while (aMask != 0)
+    {
+        aMask &= aMask - 1;
+        count++;
+    }
+
+    return count;
+}
+
+} // namespace ot
+
+#endif // NUM_UTILS_HPP_
diff --git a/src/core/common/owned_ptr.hpp b/src/core/common/owned_ptr.hpp
index ab98fd3..c1dde76 100644
--- a/src/core/common/owned_ptr.hpp
+++ b/src/core/common/owned_ptr.hpp
@@ -171,8 +171,8 @@
         return *this;
     }
 
-    OwnedPtr(const OwnedPtr &) = delete;
-    OwnedPtr(OwnedPtr &)       = delete;
+    OwnedPtr(const OwnedPtr &)            = delete;
+    OwnedPtr(OwnedPtr &)                  = delete;
     OwnedPtr &operator=(const OwnedPtr &) = delete;
 
 private:
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/examples/platforms/cc2538/system.c b/src/core/common/preference.cpp
similarity index 63%
copy from examples/platforms/cc2538/system.c
copy to src/core/common/preference.cpp
index 0d1cd63..44aeb52 100644
--- a/examples/platforms/cc2538/system.c
+++ b/src/core/common/preference.cpp
@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2016, The OpenThread Authors.
+ *  Copyright (c) 2023, The OpenThread Authors.
  *  All rights reserved.
  *
  *  Redistribution and use in source and binary forms, with or without
@@ -28,39 +28,30 @@
 
 /**
  * @file
- * @brief
- *   This file includes the platform-specific initializers.
+ *   This file implements methods for a signed preference value and its 2-bit unsigned representation.
+ *
  */
-#include "platform-cc2538.h"
-#include <openthread/config.h>
 
-otInstance *sInstance;
+#include "preference.hpp"
 
-void otSysInit(int argc, char *argv[])
+namespace ot {
+
+uint8_t Preference::To2BitUint(int8_t aPrf) { return (aPrf == 0) ? k2BitMedium : ((aPrf > 0) ? k2BitHigh : k2BitLow); }
+
+int8_t Preference::From2BitUint(uint8_t a2BitUint)
 {
-    OT_UNUSED_VARIABLE(argc);
-    OT_UNUSED_VARIABLE(argv);
+    static const int8_t kPreferences[] = {
+        /* 0 (00)  -> */ kMedium,
+        /* 1 (01)  -> */ kHigh,
+        /* 2 (10)  -> */ kMedium, // Per RFC-4191, the reserved value (10) MUST be treated as (00)
+        /* 3 (11)  -> */ kLow,
+    };
 
-#if OPENTHREAD_CONFIG_ENABLE_DEBUG_UART
-    cc2538DebugUartInit();
-#endif
-    cc2538AlarmInit();
-    cc2538RandomInit();
-    cc2538RadioInit();
+    return kPreferences[a2BitUint & k2BitMask];
 }
 
-bool otSysPseudoResetWasRequested(void)
-{
-    return false;
-}
+bool Preference::IsValid(int8_t aPrf) { return (aPrf == kHigh) || (aPrf == kMedium) || (aPrf == kLow); }
 
-void otSysProcessDrivers(otInstance *aInstance)
-{
-    sInstance = aInstance;
+const char *Preference::ToString(int8_t aPrf) { return (aPrf == 0) ? "medium" : ((aPrf > 0) ? "high" : "low"); }
 
-    // should sleep and wait for interrupts here
-
-    cc2538UartProcess();
-    cc2538RadioProcess(aInstance);
-    cc2538AlarmProcess(aInstance);
-}
+} // namespace ot
diff --git a/src/core/common/preference.hpp b/src/core/common/preference.hpp
new file mode 100644
index 0000000..cf0657d
--- /dev/null
+++ b/src/core/common/preference.hpp
@@ -0,0 +1,135 @@
+/*
+ *  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 definitions for a signed preference value and its 2-bit unsigned representation used by
+ *   Route Preference in RFC-4191.
+ */
+
+#ifndef PREFERENCE_HPP_
+#define PREFERENCE_HPP_
+
+#include "openthread-core-config.h"
+
+#include "common/error.hpp"
+
+namespace ot {
+
+/**
+ * This class provides constants and static methods to convert between `int8_t` preference and its 2-bit unsigned
+ * representation.
+ *
+ */
+class Preference
+{
+public:
+    static constexpr int8_t kHigh   = 1;  ///< High preference.
+    static constexpr int8_t kMedium = 0;  ///< Medium preference.
+    static constexpr int8_t kLow    = -1; ///< Low preference.
+
+    /**
+     * This static method converts a signed preference value to its corresponding 2-bit `uint8_t` value.
+     *
+     * A positive @p aPrf is mapped to "High Preference", a negative @p aPrf is mapped to "Low Preference", and
+     * zero @p aPrf is mapped to "Medium Preference".
+     *
+     * @param[in] aPrf   The preference to convert to `uint8_t`.
+     *
+     * @returns The 2-bit unsigned value representing @p aPrf.
+     *
+     */
+    static uint8_t To2BitUint(int8_t aPrf);
+
+    /**
+     * This static method converts a 2-bit `uint8_t` value to a signed preference value `kHigh`, `kMedium`, and `kLow`.
+     *
+     * Only the first two bits (LSB) of @p a2BitUint are used and the rest of the bits are ignored.
+     *
+     * - `0b01` (or 1) is mapped to `kHigh`.
+     * - `0b00` (or 0) is mapped to `kMedium`.
+     * - `0b11` (or 3) is mapped to `kLow`.
+     * - `0b10` (or 2) is reserved for future and is also mapped to `kMedium` (this complies with RFC-4191 where
+     *                 the reserved value `0b10` MUST be treated as `0b00` for Route Preference).
+     *
+     * @param[in] a2BitUint   The 2-bit unsigned value to convert from. Only two LSB bits are used and the reset are
+     *                        ignored.
+     *
+     * @returns The signed preference `kHigh`, `kMedium`, or `kLow` corresponding to @p a2BitUint.
+     *
+     */
+    static int8_t From2BitUint(uint8_t a2BitUint);
+
+    /**
+     * This static method indicates whether a given `int8_t` preference value is valid, i.e., whether it has of the
+     * three values `kHigh`, `kMedium`, or `kLow`.
+     *
+     * @param[in] aPrf  The signed preference value to check.
+     *
+     * @retval TRUE   if @p aPrf is valid.
+     * @retval FALSE  if @p aPrf is not valid
+     *
+     */
+    static bool IsValid(int8_t aPrf);
+
+    /**
+     * This static method indicates whether a given 2-bit `uint8_t` preference value is valid.
+     *
+     * @param[in] a2BitUint   The 2-bit unsigned value to convert from. Only two LSB bits are used and the reset are
+     *                        ignored.
+     *
+     * @retval TRUE   if the first 2 bits of @p a2BitUint are `0b00`, `0b01`, or `0b11`.
+     * @retval FALSE  if the first 2 bits of @p a2BitUint are `0b01`.
+     *
+     */
+    static bool Is2BitUintValid(uint8_t a2BitUint) { return ((a2BitUint & k2BitMask) != k2BitReserved); }
+
+    /**
+     * This static method converts a given preference to a human-readable string.
+     *
+     * @param[in] aPrf  The preference to convert.
+     *
+     * @returns The string representation of @p aPrf.
+     *
+     */
+    static const char *ToString(int8_t aPrf);
+
+    Preference(void) = delete;
+
+private:
+    static constexpr uint8_t k2BitMask = 3;
+
+    static constexpr uint8_t k2BitHigh     = 1; // 0b01
+    static constexpr uint8_t k2BitMedium   = 0; // 0b00
+    static constexpr uint8_t k2BitLow      = 3; // 0b11
+    static constexpr uint8_t k2BitReserved = 2; // 0b10
+};
+
+} // namespace ot
+
+#endif // PREFERENCE_HPP_
diff --git a/src/core/common/ptr_wrapper.hpp b/src/core/common/ptr_wrapper.hpp
index 1a65543..3704d76 100644
--- a/src/core/common/ptr_wrapper.hpp
+++ b/src/core/common/ptr_wrapper.hpp
@@ -111,7 +111,7 @@
      * @returns The wrapped pointer.
      *
      */
-    const Type *operator->(void)const { return mPointer; }
+    const Type *operator->(void) const { return mPointer; }
 
     /**
      * This method overloads the `*` dereference operator and returns a reference to the pointed object.
@@ -131,7 +131,7 @@
      * @returns A reference to the pointed object.
      *
      */
-    const Type &operator*(void)const { return *mPointer; }
+    const Type &operator*(void) const { return *mPointer; }
 
     /**
      * This method overloads the operator `==` to compare the `Ptr` with a given pointer.
diff --git a/src/core/common/random.hpp b/src/core/common/random.hpp
index d3b75d0..01fb982 100644
--- a/src/core/common/random.hpp
+++ b/src/core/common/random.hpp
@@ -110,10 +110,7 @@
  * @returns    A random `uint32_t` value.
  *
  */
-inline uint32_t GetUint32(void)
-{
-    return Manager::NonCryptoGetUint32();
-}
+inline uint32_t GetUint32(void) { return Manager::NonCryptoGetUint32(); }
 
 /**
  * This function generates and returns a random byte.
@@ -121,10 +118,7 @@
  * @returns A random `uint8_t` value.
  *
  */
-inline uint8_t GetUint8(void)
-{
-    return static_cast<uint8_t>(GetUint32() & 0xff);
-}
+inline uint8_t GetUint8(void) { return static_cast<uint8_t>(GetUint32() & 0xff); }
 
 /**
  * This function generates and returns a random `uint16_t` value.
@@ -132,10 +126,7 @@
  * @returns A random `uint16_t` value.
  *
  */
-inline uint16_t GetUint16(void)
-{
-    return static_cast<uint16_t>(GetUint32() & 0xffff);
-}
+inline uint16_t GetUint16(void) { return static_cast<uint16_t>(GetUint32() & 0xffff); }
 
 /**
  * This function generates and returns a random `uint8_t` value within a given range `[aMin, aMax)`.
@@ -209,10 +200,7 @@
  * @retval kErrorNone    Successfully filled buffer with random values.
  *
  */
-inline Error FillBuffer(uint8_t *aBuffer, uint16_t aSize)
-{
-    return Manager::CryptoFillBuffer(aBuffer, aSize);
-}
+inline Error FillBuffer(uint8_t *aBuffer, uint16_t aSize) { return Manager::CryptoFillBuffer(aBuffer, aSize); }
 
 } // namespace Crypto
 
diff --git a/src/core/common/settings.cpp b/src/core/common/settings.cpp
index 67f76e2..18f17d8 100644
--- a/src/core/common/settings.cpp
+++ b/src/core/common/settings.cpp
@@ -37,6 +37,7 @@
 #include "common/code_utils.hpp"
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
+#include "common/num_utils.hpp"
 #include "meshcop/dataset.hpp"
 #include "thread/mle.hpp"
 
@@ -53,26 +54,26 @@
 
 void SettingsBase::NetworkInfo::Log(Action aAction) const
 {
-    LogInfo("%s NetworkInfo {rloc:0x%04x, extaddr:%s, role:%s, mode:0x%02x, version:%hu, keyseq:0x%x, ...",
+    LogInfo("%s NetworkInfo {rloc:0x%04x, extaddr:%s, role:%s, mode:0x%02x, version:%u, keyseq:0x%lx, ...",
             ActionToString(aAction), GetRloc16(), GetExtAddress().ToString().AsCString(),
-            Mle::Mle::RoleToString(static_cast<Mle::DeviceRole>(GetRole())), GetDeviceMode(), GetVersion(),
-            GetKeySequence());
+            Mle::RoleToString(static_cast<Mle::DeviceRole>(GetRole())), GetDeviceMode(), GetVersion(),
+            ToUlong(GetKeySequence()));
 
-    LogInfo("... pid:0x%x, mlecntr:0x%x, maccntr:0x%x, mliid:%s}", GetPreviousPartitionId(), GetMleFrameCounter(),
-            GetMacFrameCounter(), GetMeshLocalIid().ToString().AsCString());
+    LogInfo("... pid:0x%lx, mlecntr:0x%lx, maccntr:0x%lx, mliid:%s}", ToUlong(GetPreviousPartitionId()),
+            ToUlong(GetMleFrameCounter()), ToUlong(GetMacFrameCounter()), GetMeshLocalIid().ToString().AsCString());
 }
 
 void SettingsBase::ParentInfo::Log(Action aAction) const
 {
-    LogInfo("%s ParentInfo {extaddr:%s, version:%hu}", ActionToString(aAction), GetExtAddress().ToString().AsCString(),
+    LogInfo("%s ParentInfo {extaddr:%s, version:%u}", ActionToString(aAction), GetExtAddress().ToString().AsCString(),
             GetVersion());
 }
 
 #if OPENTHREAD_FTD
 void SettingsBase::ChildInfo::Log(Action aAction) const
 {
-    LogInfo("%s ChildInfo {rloc:0x%04x, extaddr:%s, timeout:%u, mode:0x%02x, version:%hu}", ActionToString(aAction),
-            GetRloc16(), GetExtAddress().ToString().AsCString(), GetTimeout(), GetMode(), GetVersion());
+    LogInfo("%s ChildInfo {rloc:0x%04x, extaddr:%s, timeout:%lu, mode:0x%02x, version:%u}", ActionToString(aAction),
+            GetRloc16(), GetExtAddress().ToString().AsCString(), ToUlong(GetTimeout()), GetMode(), GetVersion());
 }
 #endif
 
@@ -105,6 +106,28 @@
 }
 #endif
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+Error SettingsBase::BorderAgentId::SetId(const uint8_t *aId, uint16_t aLength)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(aLength == sizeof(mId), error = kErrorInvalidArgs);
+    memcpy(mId, aId, aLength);
+
+exit:
+    return error;
+}
+
+void SettingsBase::BorderAgentId::Log(Action aAction) const
+{
+    char         buffer[sizeof(BorderAgentId) * 2 + 1];
+    StringWriter sw(buffer, sizeof(buffer));
+
+    sw.AppendHexBytes(GetId(), 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)
@@ -156,6 +179,8 @@
         "SrpServerInfo",     // (13) kKeySrpServerInfo
         "",                  // (14) Removed (previously NAT64 prefix)
         "BrUlaPrefix",       // (15) kKeyBrUlaPrefix
+        "BrOnLinkPrefixes",  // (16) kKeyBrOnLinkPrefixes
+        "BorderAgentId"      // (17) kKeyBorderAgentId
     };
 
     static_assert(1 == kKeyActiveDataset, "kKeyActiveDataset value is incorrect");
@@ -169,8 +194,10 @@
     static_assert(12 == kKeySrpClientInfo, "kKeySrpClientInfo value is incorrect");
     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 == kKeyBrUlaPrefix, "kLastKey is not valid");
+    static_assert(kLastKey == kKeyBorderAgentId, "kLastKey is not valid");
 
     OT_ASSERT(aKey <= kLastKey);
 
@@ -190,15 +217,9 @@
     SettingsBase::kKeySrpEcdsaKey,
 };
 
-void Settings::Init(void)
-{
-    Get<SettingsDriver>().Init(kSensitiveKeys, GetArrayLength(kSensitiveKeys));
-}
+void Settings::Init(void) { Get<SettingsDriver>().Init(kSensitiveKeys, GetArrayLength(kSensitiveKeys)); }
 
-void Settings::Deinit(void)
-{
-    Get<SettingsDriver>().Deinit();
-}
+void Settings::Deinit(void) { Get<SettingsDriver>().Deinit(); }
 
 void Settings::Wipe(void)
 {
@@ -308,6 +329,80 @@
 }
 #endif // OPENTHREAD_FTD
 
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+Error Settings::AddOrUpdateBrOnLinkPrefix(const BrOnLinkPrefix &aBrOnLinkPrefix)
+{
+    Error          error = kErrorNone;
+    int            index = 0;
+    BrOnLinkPrefix brPrefix;
+    bool           didUpdate = false;
+
+    while (ReadBrOnLinkPrefix(index, brPrefix) == kErrorNone)
+    {
+        if (brPrefix.GetPrefix() == aBrOnLinkPrefix.GetPrefix())
+        {
+            if (brPrefix.GetLifetime() == aBrOnLinkPrefix.GetLifetime())
+            {
+                // Existing entry fully matches `aBrOnLinkPrefix`.
+                // No need to make any changes.
+                ExitNow();
+            }
+
+            SuccessOrExit(error = Get<SettingsDriver>().Delete(kKeyBrOnLinkPrefixes, index));
+            didUpdate = true;
+            break;
+        }
+
+        index++;
+    }
+
+    SuccessOrExit(error = Get<SettingsDriver>().Add(kKeyBrOnLinkPrefixes, &aBrOnLinkPrefix, sizeof(BrOnLinkPrefix)));
+    brPrefix.Log(didUpdate ? "Updated" : "Added");
+
+exit:
+    return error;
+}
+
+Error Settings::RemoveBrOnLinkPrefix(const Ip6::Prefix &aPrefix)
+{
+    Error          error = kErrorNotFound;
+    BrOnLinkPrefix brPrefix;
+
+    for (int index = 0; ReadBrOnLinkPrefix(index, brPrefix) == kErrorNone; index++)
+    {
+        if (brPrefix.GetPrefix() == aPrefix)
+        {
+            SuccessOrExit(error = Get<SettingsDriver>().Delete(kKeyBrOnLinkPrefixes, index));
+            brPrefix.Log("Removed");
+            break;
+        }
+    }
+
+exit:
+    return error;
+}
+
+Error Settings::DeleteAllBrOnLinkPrefixes(void) { return Get<SettingsDriver>().Delete(kKeyBrOnLinkPrefixes); }
+
+Error Settings::ReadBrOnLinkPrefix(int aIndex, BrOnLinkPrefix &aBrOnLinkPrefix)
+{
+    uint16_t length = sizeof(BrOnLinkPrefix);
+
+    aBrOnLinkPrefix.Init();
+
+    return Get<SettingsDriver>().Get(kKeyBrOnLinkPrefixes, aIndex, &aBrOnLinkPrefix, &length);
+}
+
+void Settings::BrOnLinkPrefix::Log(const char *aActionText) const
+{
+    OT_UNUSED_VARIABLE(aActionText);
+
+    LogInfo("%s %s entry {prefix:%s,lifetime:%lu}", aActionText, KeyToString(kKeyBrOnLinkPrefixes),
+            GetPrefix().ToString().AsCString(), ToUlong(GetLifetime()));
+}
+
+#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+
 Error Settings::ReadEntry(Key aKey, void *aValue, uint16_t aMaxLength) const
 {
     Error    error;
@@ -447,6 +542,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 6db4a02..4b307d0 100644
--- a/src/core/common/settings.hpp
+++ b/src/core/common/settings.hpp
@@ -36,6 +36,7 @@
 
 #include "openthread-core-config.h"
 
+#include <openthread/border_agent.h>
 #include <openthread/platform/settings.h>
 
 #include "common/clearable.hpp"
@@ -49,6 +50,7 @@
 #include "mac/mac_types.hpp"
 #include "meshcop/dataset.hpp"
 #include "net/ip6_address.hpp"
+#include "thread/version.hpp"
 #include "utils/flash.hpp"
 #include "utils/slaac_address.hpp"
 
@@ -118,9 +120,12 @@
         kKeySrpClientInfo     = OT_SETTINGS_KEY_SRP_CLIENT_INFO,
         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 = kKeyBrUlaPrefix; ///< 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");
 
@@ -132,6 +137,7 @@
     class NetworkInfo : private Clearable<NetworkInfo>
     {
         friend class Settings;
+        friend class Clearable<NetworkInfo>;
 
     public:
         static constexpr Key kKey = kKeyNetworkInfo; ///< The associated key.
@@ -143,7 +149,7 @@
         void Init(void)
         {
             Clear();
-            SetVersion(OT_THREAD_VERSION_1_1);
+            SetVersion(kThreadVersion1p1);
         }
 
         /**
@@ -338,6 +344,7 @@
     class ParentInfo : private Clearable<ParentInfo>
     {
         friend class Settings;
+        friend class Clearable<ParentInfo>;
 
     public:
         static constexpr Key kKey = kKeyParentInfo; ///< The associated key.
@@ -349,7 +356,7 @@
         void Init(void)
         {
             Clear();
-            SetVersion(OT_THREAD_VERSION_1_1);
+            SetVersion(kThreadVersion1p1);
         }
 
         /**
@@ -411,7 +418,7 @@
         void Init(void)
         {
             memset(this, 0, sizeof(*this));
-            SetVersion(OT_THREAD_VERSION_1_1);
+            SetVersion(kThreadVersion1p1);
         }
 
         /**
@@ -531,6 +538,7 @@
     class DadInfo : private Clearable<DadInfo>
     {
         friend class Settings;
+        friend class Clearable<DadInfo>;
 
     public:
         static constexpr Key kKey = kKeyDadInfo; ///< The associated key.
@@ -579,7 +587,65 @@
     private:
         BrUlaPrefix(void) = default;
     };
-#endif
+
+    /**
+     * This class represents a BR on-link prefix entry for settings storage.
+     *
+     */
+    OT_TOOL_PACKED_BEGIN
+    class BrOnLinkPrefix : public Clearable<BrOnLinkPrefix>
+    {
+        friend class Settings;
+
+    public:
+        static constexpr Key kKey = kKeyBrOnLinkPrefixes; ///< The associated key.
+
+        /**
+         * This method initializes the `BrOnLinkPrefix` object.
+         *
+         */
+        void Init(void) { Clear(); }
+
+        /**
+         * This method gets the prefix.
+         *
+         * @returns The prefix.
+         *
+         */
+        const Ip6::Prefix &GetPrefix(void) const { return mPrefix; }
+
+        /**
+         * This method set the prefix.
+         *
+         * @param[in] aPrefix   The prefix.
+         *
+         */
+        void SetPrefix(const Ip6::Prefix &aPrefix) { mPrefix = aPrefix; }
+
+        /**
+         * This method gets the remaining prefix lifetime in seconds.
+         *
+         * @returns The prefix lifetime in seconds.
+         *
+         */
+        uint32_t GetLifetime(void) const { return mLifetime; }
+
+        /**
+         * This method sets the the prefix lifetime.
+         *
+         * @param[in] aLifetime  The prefix lifetime in seconds.
+         *
+         */
+        void SetLifetime(uint32_t aLifetime) { mLifetime = aLifetime; }
+
+    private:
+        void Log(const char *aActionText) const;
+
+        Ip6::Prefix mPrefix;
+        uint32_t    mLifetime;
+    } OT_TOOL_PACKED_END;
+
+#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 
 #if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
     /**
@@ -606,6 +672,7 @@
     class SrpClientInfo : private Clearable<SrpClientInfo>
     {
         friend class Settings;
+        friend class Clearable<SrpClientInfo>;
 
     public:
         static constexpr Key kKey = kKeySrpClientInfo; ///< The associated key.
@@ -666,6 +733,7 @@
     class SrpServerInfo : private Clearable<SrpServerInfo>
     {
         friend class Settings;
+        friend class Clearable<SrpServerInfo>;
 
     public:
         static constexpr Key kKey = kKeySrpServerInfo; ///< The associated key.
@@ -699,6 +767,59 @@
     } 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 : private Clearable<BorderAgentId>
+    {
+        friend class Settings;
+        friend class Clearable<BorderAgentId>;
+
+    public:
+        static constexpr Key     kKey    = kKeyBorderAgentId; ///< The associated key.
+        static constexpr uint8_t kLength = OT_BORDER_AGENT_ID_LENGTH;
+
+        /**
+         * This method initializes the `BorderAgentId` object.
+         *
+         */
+        void Init(void) { Clear(); }
+
+        /**
+         * This method returns the Border Agent ID.
+         *
+         * @returns The Border Agent ID.
+         *
+         */
+        const uint8_t *GetId(void) const { return mId; }
+
+        /**
+         * This method returns the Border Agent ID.
+         *
+         * @returns The Border Agent ID.
+         *
+         */
+        uint8_t *GetId(void) { return mId; }
+
+        /**
+         * This method sets the Border Agent ID.
+         *
+         * @retval kErrorInvalidArgs If `aLength` doesn't equal to `OT_BORDER_AGENT_ID_LENGTH`.
+         * @retval kErrorNone        If success.
+         *
+         */
+        Error SetId(const uint8_t *aId, uint16_t aLength);
+
+    private:
+        void Log(Action aAction) const;
+
+        uint8_t mId[kLength];
+    } OT_TOOL_PACKED_END;
+#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+
 protected:
     explicit SettingsBase(Instance &aInstance)
         : InstanceLocator(aInstance)
@@ -1024,7 +1145,7 @@
          * @returns A reference to the `ChildInfo` entry currently pointed by the iterator.
          *
          */
-        const ChildInfo &operator*(void)const { return mChildInfo; }
+        const ChildInfo &operator*(void) const { return mChildInfo; }
 
         /**
          * This method overloads operator `==` to evaluate whether or not two iterator instances are equal.
@@ -1062,6 +1183,56 @@
     };
 #endif // OPENTHREAD_FTD
 
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    /**
+     * This method adds or updates an on-link prefix.
+     *
+     * If there is no matching entry (matching the same `GetPrefix()`) saved in `Settings`, the new entry will be added.
+     * If there is matching entry, it will be updated to the new @p aPrefix.
+     *
+     * @param[in] aBrOnLinkPrefix    The on-link prefix to save (add or updated).
+     *
+     * @retval kErrorNone             Successfully added or updated the entry in settings.
+     * @retval kErrorNotImplemented   The platform does not implement settings functionality.
+     *
+     */
+    Error AddOrUpdateBrOnLinkPrefix(const BrOnLinkPrefix &aBrOnLinkPrefix);
+
+    /**
+     * This method removes an on-link prefix entry matching a given prefix.
+     *
+     * @param[in] aPrefix            The prefix to remove
+     *
+     * @retval kErrorNone            Successfully removed the matching entry in settings.
+     * @retval kErrorNotImplemented  The platform does not implement settings functionality.
+     *
+     */
+    Error RemoveBrOnLinkPrefix(const Ip6::Prefix &aPrefix);
+
+    /**
+     * This method deletes all on-link prefix entries from the settings.
+     *
+     * @retval kErrorNone            Successfully deleted the entries.
+     * @retval kErrorNotImplemented  The platform does not implement settings functionality.
+     *
+     */
+    Error DeleteAllBrOnLinkPrefixes(void);
+
+    /**
+     * This method retrieves an entry from on-link prefixes list at a given index.
+     *
+     * @param[in]  aIndex            The index to read.
+     * @param[out] aBrOnLinkPrefix   A reference to `BrOnLinkPrefix` to output the read value.
+     *
+     * @retval kErrorNone             Successfully read the value.
+     * @retval kErrorNotFound         No corresponding value in the setting store.
+     * @retval kErrorNotImplemented   The platform does not implement settings functionality.
+     *
+     */
+    Error ReadBrOnLinkPrefix(int aIndex, BrOnLinkPrefix &aBrOnLinkPrefix);
+
+#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+
 private:
 #if OPENTHREAD_FTD
     class ChildInfoIteratorBuilder : public InstanceLocator
diff --git a/src/core/common/string.cpp b/src/core/common/string.cpp
index 4940076..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++)
@@ -255,10 +281,7 @@
     return *this;
 }
 
-bool IsValidUtf8String(const char *aString)
-{
-    return IsValidUtf8String(aString, strlen(aString));
-}
+bool IsValidUtf8String(const char *aString) { return IsValidUtf8String(aString, strlen(aString)); }
 
 bool IsValidUtf8String(const char *aString, size_t aLength)
 {
diff --git a/src/core/common/string.hpp b/src/core/common/string.hpp
index a2ab1aa..fbbaca7 100644
--- a/src/core/common/string.hpp
+++ b/src/core/common/string.hpp
@@ -43,6 +43,7 @@
 #include "common/binary_search.hpp"
 #include "common/code_utils.hpp"
 #include "common/error.hpp"
+#include "common/num_utils.hpp"
 
 namespace ot {
 
@@ -143,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.
@@ -156,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.
@@ -312,7 +352,7 @@
      * @returns The string writer.
      *
      */
-    StringWriter &Append(const char *aFormat, ...);
+    StringWriter &Append(const char *aFormat, ...) OT_TOOL_PRINTF_STYLE_FORMAT_ARG_CHECK(2, 3);
 
     /**
      * This method appends `printf()` style formatted data to the buffer.
@@ -349,7 +389,7 @@
     void ConvertToUppercase(void) { StringConvertToUppercase(mBuffer); }
 
 private:
-    char *         mBuffer;
+    char          *mBuffer;
     uint16_t       mLength;
     const uint16_t mSize;
 };
@@ -405,7 +445,7 @@
         const char *mString; ///< The associated string.
 
     private:
-        int Compare(uint16_t aKey) const { return (aKey == mKey) ? 0 : ((aKey > mKey) ? 1 : -1); }
+        int Compare(uint16_t aKey) const { return ThreeWayCompare(aKey, mKey); }
 
         constexpr static bool AreInOrder(const Entry &aFirst, const Entry &aSecond)
         {
diff --git a/src/core/common/tasklet.hpp b/src/core/common/tasklet.hpp
index 6899e57..3694eab 100644
--- a/src/core/common/tasklet.hpp
+++ b/src/core/common/tasklet.hpp
@@ -150,6 +150,33 @@
 };
 
 /**
+ * This template class defines a tasklet owned by specific type and using a method on owner type as the callback.
+ *
+ * @tparam Owner              The type of owner of this tasklet.
+ * @tparam HandleTaskletPtr   A pointer to a non-static member method of `Owner` to use as tasklet handler.
+ *
+ * The `Owner` MUST be a type that is accessible using `InstanceLocator::Get<Owner>()`.
+ *
+ */
+template <typename Owner, void (Owner::*HandleTaskletPtr)(void)> class TaskletIn : public Tasklet
+{
+public:
+    /**
+     * This constructor initializes the tasklet.
+     *
+     * @param[in]  aInstance   The OpenThread instance.
+     *
+     */
+    explicit TaskletIn(Instance &aInstance)
+        : Tasklet(aInstance, HandleTasklet)
+    {
+    }
+
+private:
+    static void HandleTasklet(Tasklet &aTasklet); // Implemented in `locator_getters.hpp`
+};
+
+/**
  * This class defines a tasklet that also maintains a user context pointer.
  *
  * In typical `Tasklet` use, in the handler callback, the owner of the tasklet is determined using `GetOwner<Type>`
diff --git a/src/core/common/time_ticker.cpp b/src/core/common/time_ticker.cpp
index 2bfb130..21845c8 100644
--- a/src/core/common/time_ticker.cpp
+++ b/src/core/common/time_ticker.cpp
@@ -45,7 +45,7 @@
 TimeTicker::TimeTicker(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mReceivers(0)
-    , mTimer(aInstance, HandleTimer)
+    , mTimer(aInstance)
 {
 }
 
@@ -69,11 +69,6 @@
     }
 }
 
-void TimeTicker::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<TimeTicker>().HandleTimer();
-}
-
 void TimeTicker::HandleTimer(void)
 {
     mTimer.FireAt(mTimer.GetFireTime() + Random::NonCrypto::AddJitter(kTickInterval, kRestartJitter));
@@ -101,12 +96,10 @@
     }
 #endif
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
     if (mReceivers & Mask(kChildSupervisor))
     {
-        Get<Utils::ChildSupervisor>().HandleTimeTick();
+        Get<ChildSupervisor>().HandleTimeTick();
     }
-#endif
 #endif // OPENTHREAD_FTD
 
 #if OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE
@@ -129,6 +122,11 @@
         Get<MlrManager>().HandleTimeTick();
     }
 #endif
+
+    if (mReceivers & Mask(kIp6Mpl))
+    {
+        Get<Ip6::Mpl>().HandleTimeTick();
+    }
 }
 
 } // namespace ot
diff --git a/src/core/common/time_ticker.hpp b/src/core/common/time_ticker.hpp
index a89f3e3..0db6df2 100644
--- a/src/core/common/time_ticker.hpp
+++ b/src/core/common/time_ticker.hpp
@@ -67,11 +67,12 @@
         kMeshForwarder,          ///< `MeshForwarder`
         kMleRouter,              ///< `Mle::MleRouter`
         kAddressResolver,        ///< `AddressResolver`
-        kChildSupervisor,        ///< `Utils::ChildSupervisor`
+        kChildSupervisor,        ///< `ChildSupervisor`
         kIp6FragmentReassembler, ///< `Ip6::Ip6` (handling of fragmented messages)
         kDuaManager,             ///< `DuaManager`
         kMlrManager,             ///< `MlrManager`
         kNetworkDataNotifier,    ///< `NetworkData::Notifier`
+        kIp6Mpl,                 ///< `Ip6::Mpl`
 
         kNumReceivers, ///< Number of receivers.
     };
@@ -115,11 +116,12 @@
 
     constexpr static uint32_t Mask(Receiver aReceiver) { return static_cast<uint32_t>(1U) << aReceiver; }
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTimer(void);
 
-    uint32_t   mReceivers;
-    TimerMilli mTimer;
+    using TickerTimer = TimerMilliIn<TimeTicker, &TimeTicker::HandleTimer>;
+
+    uint32_t    mReceivers;
+    TickerTimer mTimer;
 
     static_assert(kNumReceivers < sizeof(mReceivers) * CHAR_BIT, "Too many `Receiver`s - does not fit in a bit mask");
 };
diff --git a/src/core/common/timer.cpp b/src/core/common/timer.cpp
index 66de06d..7722cae 100644
--- a/src/core/common/timer.cpp
+++ b/src/core/common/timer.cpp
@@ -77,10 +77,7 @@
     return retval;
 }
 
-void TimerMilli::Start(uint32_t aDelay)
-{
-    StartAt(GetNow(), aDelay);
-}
+void TimerMilli::Start(uint32_t aDelay) { StartAt(GetNow(), aDelay); }
 
 void TimerMilli::StartAt(TimeMilli aStartTime, uint32_t aDelay)
 {
@@ -102,15 +99,9 @@
     }
 }
 
-void TimerMilli::Stop(void)
-{
-    Get<Scheduler>().Remove(*this);
-}
+void TimerMilli::Stop(void) { Get<Scheduler>().Remove(*this); }
 
-void TimerMilli::RemoveAll(Instance &aInstance)
-{
-    aInstance.Get<Scheduler>().RemoveAll();
-}
+void TimerMilli::RemoveAll(Instance &aInstance) { aInstance.Get<Scheduler>().RemoveAll(); }
 
 void Timer::Scheduler::Add(Timer &aTimer, const AlarmApi &aAlarmApi)
 {
@@ -168,7 +159,7 @@
     }
     else
     {
-        Timer *  timer = mTimerList.GetHead();
+        Timer   *timer = mTimerList.GetHead();
         Time     now(aAlarmApi.AlarmGetNow());
         uint32_t remaining;
 
@@ -228,10 +219,7 @@
     &otPlatAlarmMicroGetNow,
 };
 
-void TimerMicro::Start(uint32_t aDelay)
-{
-    StartAt(GetNow(), aDelay);
-}
+void TimerMicro::Start(uint32_t aDelay) { StartAt(GetNow(), aDelay); }
 
 void TimerMicro::StartAt(TimeMicro aStartTime, uint32_t aDelay)
 {
@@ -245,15 +233,9 @@
     Get<Scheduler>().Add(*this);
 }
 
-void TimerMicro::Stop(void)
-{
-    Get<Scheduler>().Remove(*this);
-}
+void TimerMicro::Stop(void) { Get<Scheduler>().Remove(*this); }
 
-void TimerMicro::RemoveAll(Instance &aInstance)
-{
-    aInstance.Get<Scheduler>().RemoveAll();
-}
+void TimerMicro::RemoveAll(Instance &aInstance) { aInstance.Get<Scheduler>().RemoveAll(); }
 
 extern "C" void otPlatAlarmMicroFired(otInstance *aInstance)
 {
diff --git a/src/core/common/timer.hpp b/src/core/common/timer.hpp
index 3faf09f..34e60ec 100644
--- a/src/core/common/timer.hpp
+++ b/src/core/common/timer.hpp
@@ -140,7 +140,7 @@
 
     Handler mHandler;
     Time    mFireTime;
-    Timer * mNext;
+    Timer  *mNext;
 };
 
 extern "C" void otPlatAlarmMilliFired(otInstance *aInstance);
@@ -247,6 +247,33 @@
 };
 
 /**
+ * This template class defines a timer owned by a specific type and using a method on owner type as the callback.
+ *
+ * @tparam Owner              The type of the owner of this timer.
+ * @tparam HandleTimerPtr     A pointer to a non-static member method of `Owner` to use as timer handler.
+ *
+ * The `Owner` MUST be a type that is accessible using `InstanceLocator::Get<Owner>()`.
+ *
+ */
+template <typename Owner, void (Owner::*HandleTimerPtr)(void)> class TimerMilliIn : public TimerMilli
+{
+public:
+    /**
+     * This constructor initializes the timer.
+     *
+     * @param[in]  aInstance   The OpenThread instance.
+     *
+     */
+    explicit TimerMilliIn(Instance &aInstance)
+        : TimerMilli(aInstance, HandleTimer)
+    {
+    }
+
+private:
+    static void HandleTimer(Timer &aTimer); // Implemented in `locator_getters.hpp`
+};
+
+/**
  * This class implements a millisecond timer that also maintains a user context pointer.
  *
  * In typical `TimerMilli`/`TimerMicro` use, in the timer callback handler, the owner of the timer is determined using
@@ -379,6 +406,34 @@
 protected:
     static void RemoveAll(Instance &aInstance);
 };
+
+/**
+ * This template class defines a timer owned by a specific type and using a method on owner type as the callback.
+ *
+ * @tparam Owner              The type of the owner of this timer.
+ * @tparam HandleTimerPtr     A pointer to a non-static member method of `Owner` to use as timer handler.
+ *
+ * The `Owner` MUST be a type that is accessible using `InstanceLocator::Get<Owner>()`.
+ *
+ */
+template <typename Owner, void (Owner::*HandleTimerPtr)(void)> class TimerMicroIn : public TimerMicro
+{
+public:
+    /**
+     * This constructor initializes the timer.
+     *
+     * @param[in]  aInstance   The OpenThread instance.
+     *
+     */
+    explicit TimerMicroIn(Instance &aInstance)
+        : TimerMicro(aInstance, HandleTimer)
+    {
+    }
+
+private:
+    static void HandleTimer(Timer &aTimer); // Implemented in `locator_getters.hpp`
+};
+
 #endif // OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
 
 /**
diff --git a/src/core/common/tlvs.cpp b/src/core/common/tlvs.cpp
index c045db6..2d4f018 100644
--- a/src/core/common/tlvs.cpp
+++ b/src/core/common/tlvs.cpp
@@ -54,25 +54,21 @@
     return reinterpret_cast<const uint8_t *>(this) + (IsExtended() ? sizeof(ExtendedTlv) : sizeof(Tlv));
 }
 
-Error Tlv::AppendTo(Message &aMessage) const
-{
-    return aMessage.AppendBytes(this, static_cast<uint16_t>(GetSize()));
-}
+Error Tlv::AppendTo(Message &aMessage) const { return aMessage.AppendBytes(this, static_cast<uint16_t>(GetSize())); }
 
 Error Tlv::FindTlv(const Message &aMessage, uint8_t aType, uint16_t aMaxSize, Tlv &aTlv)
 {
-    Error    error;
-    uint16_t offset;
-    uint16_t size;
+    Error      error;
+    ParsedInfo info;
 
-    SuccessOrExit(error = Find(aMessage, aType, &offset, &size, nullptr));
+    SuccessOrExit(error = info.FindIn(aMessage, aType));
 
-    if (aMaxSize > size)
+    if (aMaxSize > info.mSize)
     {
-        aMaxSize = size;
+        aMaxSize = info.mSize;
     }
 
-    aMessage.ReadBytes(offset, &aTlv, aMaxSize);
+    aMessage.ReadBytes(info.mOffset, &aTlv, aMaxSize);
 
 exit:
     return error;
@@ -80,97 +76,124 @@
 
 Error Tlv::FindTlvOffset(const Message &aMessage, uint8_t aType, uint16_t &aOffset)
 {
-    return Find(aMessage, aType, &aOffset, nullptr, nullptr);
+    Error      error;
+    ParsedInfo info;
+
+    SuccessOrExit(error = info.FindIn(aMessage, aType));
+    aOffset = info.mOffset;
+
+exit:
+    return error;
 }
 
 Error Tlv::FindTlvValueOffset(const Message &aMessage, uint8_t aType, uint16_t &aValueOffset, uint16_t &aLength)
 {
-    Error    error;
-    uint16_t offset;
-    uint16_t size;
-    bool     isExtendedTlv;
+    Error      error;
+    ParsedInfo info;
 
-    SuccessOrExit(error = Find(aMessage, aType, &offset, &size, &isExtendedTlv));
+    SuccessOrExit(error = info.FindIn(aMessage, aType));
 
-    if (!isExtendedTlv)
+    aValueOffset = info.mValueOffset;
+    aLength      = info.mLength;
+
+exit:
+    return error;
+}
+
+Error Tlv::ParsedInfo::ParseFrom(const Message &aMessage, uint16_t aOffset)
+{
+    // This method reads and parses the TLV info from `aMessage` at
+    // `aOffset`. This can be used independent of whether the TLV is
+    // extended or not. It validates that the entire TLV can be read
+    // from `aMessage`.  Returns `kErrorNone` when successfully parsed,
+    // otherwise `kErrorParse`.
+
+    Error       error;
+    Tlv         tlv;
+    ExtendedTlv extTlv;
+    uint16_t    headerSize;
+
+    SuccessOrExit(error = aMessage.Read(aOffset, tlv));
+
+    if (!tlv.IsExtended())
     {
-        aValueOffset = offset + sizeof(Tlv);
-        aLength      = size - sizeof(Tlv);
+        mType      = tlv.GetType();
+        mLength    = tlv.GetLength();
+        headerSize = sizeof(Tlv);
     }
     else
     {
-        aValueOffset = offset + sizeof(ExtendedTlv);
-        aLength      = size - sizeof(ExtendedTlv);
+        SuccessOrExit(error = aMessage.Read(aOffset, extTlv));
+
+        mType      = extTlv.GetType();
+        mLength    = extTlv.GetLength();
+        headerSize = sizeof(ExtendedTlv);
+    }
+
+    // We know that we could successfully read `tlv` or `extTlv`
+    // (`headerSize` bytes) from the message, so the calculation of the
+    // remaining length as `aMessage.GetLength() - aOffset - headerSize`
+    // cannot underflow.
+
+    VerifyOrExit(mLength <= aMessage.GetLength() - aOffset - headerSize, error = kErrorParse);
+
+    // Now that we know the entire TLV is contained within the
+    // `aMessage`, we can safely calculate `mValueOffset` and `mSize`
+    // as `uint16_t` and know that there will be no overflow.
+
+    mType        = tlv.GetType();
+    mOffset      = aOffset;
+    mValueOffset = aOffset + headerSize;
+    mSize        = mLength + headerSize;
+
+exit:
+    return error;
+}
+
+Error Tlv::ParsedInfo::FindIn(const Message &aMessage, uint8_t aType)
+{
+    // This  method searches within `aMessage` starting from
+    // `aMessage.GetOffset()` for a TLV type `aType` and parsed its
+    // info and validates that the entire TLV can be read from
+    // `aMessage`. Returns `kErrorNone` when found, otherwise
+    // `kErrorNotFound`.
+
+    Error    error  = kErrorNotFound;
+    uint16_t offset = aMessage.GetOffset();
+
+    while (true)
+    {
+        SuccessOrExit(ParseFrom(aMessage, offset));
+
+        if (mType == aType)
+        {
+            error = kErrorNone;
+            ExitNow();
+        }
+
+        // `ParseFrom()` already validated that `offset + mSize` is
+        // less than `aMessage.GetLength()` and therefore we can not
+        // have an overflow here.
+
+        offset += mSize;
     }
 
 exit:
     return error;
 }
 
-Error Tlv::Find(const Message &aMessage, uint8_t aType, uint16_t *aOffset, uint16_t *aSize, bool *aIsExtendedTlv)
+Error Tlv::ReadStringTlv(const Message &aMessage, uint16_t aOffset, uint8_t aMaxStringLength, char *aValue)
 {
-    // This static method searches within a `aMessage` for a TLV type
-    // `aType` and outputs the TLV offset, size, and whether or not it
-    // is an Extended TLV.
-    //
-    // A `nullptr` pointer can be used for output parameters `aOffset`,
-    // `aSize`, or `aIsExtendedTlv` if the parameter is not required.
-    //
-    // Returns `kErrorNone` when found, otherwise `kErrorNotFound`.
+    Error      error = kErrorNone;
+    ParsedInfo info;
+    uint16_t   length;
 
-    Error    error        = kErrorNotFound;
-    uint16_t offset       = aMessage.GetOffset();
-    uint16_t remainingLen = aMessage.GetLength();
-    Tlv      tlv;
-    uint32_t size;
+    SuccessOrExit(error = info.ParseFrom(aMessage, aOffset));
 
-    VerifyOrExit(offset <= remainingLen);
-    remainingLen -= offset;
+    length = Min(info.mLength, static_cast<uint16_t>(aMaxStringLength));
 
-    while (true)
-    {
-        SuccessOrExit(aMessage.Read(offset, tlv));
-
-        if (tlv.mLength != kExtendedLength)
-        {
-            size = tlv.GetSize();
-        }
-        else
-        {
-            ExtendedTlv extTlv;
-
-            SuccessOrExit(aMessage.Read(offset, extTlv));
-
-            VerifyOrExit(extTlv.GetLength() <= (remainingLen - sizeof(ExtendedTlv)));
-            size = extTlv.GetSize();
-        }
-
-        VerifyOrExit(size <= remainingLen);
-
-        if (tlv.GetType() == aType)
-        {
-            if (aOffset != nullptr)
-            {
-                *aOffset = offset;
-            }
-
-            if (aSize != nullptr)
-            {
-                *aSize = static_cast<uint16_t>(size);
-            }
-
-            if (aIsExtendedTlv != nullptr)
-            {
-                *aIsExtendedTlv = (tlv.mLength == kExtendedLength);
-            }
-
-            error = kErrorNone;
-            ExitNow();
-        }
-
-        offset += size;
-        remainingLen -= size;
-    }
+    aMessage.ReadBytes(info.mValueOffset, aValue, length);
+    aValue[length] = '\0';
 
 exit:
     return error;
@@ -180,7 +203,7 @@
 {
     Error error;
 
-    SuccessOrExit(error = ReadTlv(aMessage, aOffset, &aValue, sizeof(aValue)));
+    SuccessOrExit(error = ReadTlvValue(aMessage, aOffset, &aValue, sizeof(aValue)));
     aValue = Encoding::BigEndian::HostSwap<UintType>(aValue);
 
 exit:
@@ -192,16 +215,28 @@
 template Error Tlv::ReadUintTlv<uint16_t>(const Message &aMessage, uint16_t aOffset, uint16_t &aValue);
 template Error Tlv::ReadUintTlv<uint32_t>(const Message &aMessage, uint16_t aOffset, uint32_t &aValue);
 
-Error Tlv::ReadTlv(const Message &aMessage, uint16_t aOffset, void *aValue, uint8_t aMinLength)
+Error Tlv::ReadTlvValue(const Message &aMessage, uint16_t aOffset, void *aValue, uint8_t aMinLength)
 {
-    Error error = kErrorNone;
-    Tlv   tlv;
+    Error      error;
+    ParsedInfo info;
 
-    SuccessOrExit(error = aMessage.Read(aOffset, tlv));
-    VerifyOrExit(!tlv.IsExtended() && (tlv.GetLength() >= aMinLength), error = kErrorParse);
-    VerifyOrExit(tlv.GetSize() + aOffset <= aMessage.GetLength(), error = kErrorParse);
+    SuccessOrExit(error = info.ParseFrom(aMessage, aOffset));
 
-    aMessage.ReadBytes(aOffset + sizeof(Tlv), aValue, aMinLength);
+    VerifyOrExit(info.mLength >= aMinLength, error = kErrorParse);
+
+    aMessage.ReadBytes(info.mValueOffset, aValue, aMinLength);
+
+exit:
+    return error;
+}
+
+Error Tlv::FindStringTlv(const Message &aMessage, uint8_t aType, uint8_t aMaxStringLength, char *aValue)
+{
+    Error    error = kErrorNone;
+    uint16_t offset;
+
+    SuccessOrExit(error = FindTlvOffset(aMessage, aType, offset));
+    error = ReadStringTlv(aMessage, offset, aMaxStringLength, aValue);
 
 exit:
     return error;
@@ -238,6 +273,13 @@
     return error;
 }
 
+Error Tlv::AppendStringTlv(Message &aMessage, uint8_t aType, uint8_t aMaxStringLength, const char *aValue)
+{
+    uint16_t length = (aValue == nullptr) ? 0 : StringLength(aValue, aMaxStringLength);
+
+    return AppendTlv(aMessage, aType, aValue, static_cast<uint8_t>(length));
+}
+
 template <typename UintType> Error Tlv::AppendUintTlv(Message &aMessage, uint8_t aType, UintType aValue)
 {
     UintType value = Encoding::BigEndian::HostSwap<UintType>(aValue);
diff --git a/src/core/common/tlvs.hpp b/src/core/common/tlvs.hpp
index 4dd55f4..577ce4a 100644
--- a/src/core/common/tlvs.hpp
+++ b/src/core/common/tlvs.hpp
@@ -175,7 +175,9 @@
     Error AppendTo(Message &aMessage) const;
 
     /**
-     * This static method reads a TLV in a message at a given offset expecting a minimum length for the value.
+     * This static method reads a TLV's value in a message at a given offset expecting a minimum length for the value.
+     *
+     * This method can be used independent of whether the read TLV (from the message) is an Extended TLV or not.
      *
      * @param[in]   aMessage    The message to read from.
      * @param[in]   aOffset     The offset into the message pointing to the start of the TLV.
@@ -186,7 +188,7 @@
      * @retval kErrorParse       The TLV was not well-formed and could not be parsed.
      *
      */
-    static Error ReadTlv(const Message &aMessage, uint16_t aOffset, void *aValue, uint8_t aMinLength);
+    static Error ReadTlvValue(const Message &aMessage, uint16_t aOffset, void *aValue, uint8_t aMinLength);
 
     /**
      * This static method reads a simple TLV with a single non-integral value in a message at a given offset.
@@ -204,7 +206,7 @@
     template <typename SimpleTlvType>
     static Error Read(const Message &aMessage, uint16_t aOffset, typename SimpleTlvType::ValueType &aValue)
     {
-        return ReadTlv(aMessage, aOffset, &aValue, sizeof(aValue));
+        return ReadTlvValue(aMessage, aOffset, &aValue, sizeof(aValue));
     }
 
     /**
@@ -227,6 +229,25 @@
     }
 
     /**
+     * This static method reads a simple TLV with a UTF-8 string value in a message at a given offset.
+     *
+     * @tparam      StringTlvType   The simple TLV type to read (must be a sub-class of `StringTlvInfo`).
+     *
+     * @param[in]   aMessage        The message to read from.
+     * @param[in]   aOffset         The offset into the message pointing to the start of the TLV.
+     * @param[out]  aValue          A reference to the string buffer to output the read value.
+     *
+     * @retval kErrorNone        Successfully read the TLV and updated the @p aValue.
+     * @retval kErrorParse       The TLV was not well-formed and could not be parsed.
+     *
+     */
+    template <typename StringTlvType>
+    static Error Read(const Message &aMessage, uint16_t aOffset, typename StringTlvType::StringType &aValue)
+    {
+        return ReadStringTlv(aMessage, aOffset, StringTlvType::kMaxStringLength, aValue);
+    }
+
+    /**
      * This static method searches for and reads a requested TLV out of a given message.
      *
      * This method can be used independent of whether the read TLV (from message) is an Extended TLV or not.
@@ -368,6 +389,32 @@
     }
 
     /**
+     * This static method searches for a simple TLV with a UTF-8 string value in a message, and then reads its value
+     * into a given string buffer.
+     *
+     * If the TLV length is longer than maximum string length specified by `StringTlvType::kMaxStringLength` then
+     * only up to maximum length is read and returned. In this case `kErrorNone` is returned.
+     *
+     * The returned string in @p aValue is always null terminated.`StringTlvType::StringType` MUST have at least
+     * `kMaxStringLength + 1` chars.
+     *
+     * @tparam       StringTlvType  The simple TLV type to find (must be a sub-class of `StringTlvInfo`)
+     *
+     * @param[in]    aMessage        A reference to the message.
+     * @param[out]   aValue          A reference to a string buffer to output the TLV's value.
+     *
+     * @retval kErrorNone         The TLV was found and read successfully. @p aValue is updated.
+     * @retval kErrorNotFound     Could not find the TLV with Type @p aType.
+     * @retval kErrorParse        TLV was found but it was not well-formed and could not be parsed.
+     *
+     */
+    template <typename StringTlvType>
+    static Error Find(const Message &aMessage, typename StringTlvType::StringType &aValue)
+    {
+        return FindStringTlv(aMessage, StringTlvType::kType, StringTlvType::kMaxStringLength, aValue);
+    }
+
+    /**
      * This static method appends a TLV with a given type and value to a message.
      *
      * On success this method grows the message by the size of the TLV.
@@ -426,13 +473,51 @@
         return AppendUintTlv(aMessage, UintTlvType::kType, aValue);
     }
 
+    /**
+     * This static method appends a simple TLV with a single UTF-8 string value to a message.
+     *
+     * On success this method grows the message by the size of the TLV.
+     *
+     * If the passed in @p aValue string length is longer than the maximum allowed length for the TLV as specified by
+     * `StringTlvType::kMaxStringLength`, the first maximum length chars are appended.
+     *
+     * The @p aValue can be `nullptr` in which case it is treated as an empty string.
+     *
+     * @tparam     StringTlvType  The simple TLV type to append (must be a sub-class of `StringTlvInfo`)
+     *
+     * @param[in]  aMessage       A reference to the message to append to.
+     * @param[in]  aValue         A pointer to a C string to append as TLV's value.
+     *
+     * @retval kErrorNone     Successfully appended the TLV to the message.
+     * @retval kErrorNoBufs   Insufficient available buffers to grow the message.
+     *
+     */
+    template <typename StringTlvType> static Error Append(Message &aMessage, const char *aValue)
+    {
+        return AppendStringTlv(aMessage, StringTlvType::kType, StringTlvType::kMaxStringLength, aValue);
+    }
+
 protected:
     static const uint8_t kExtendedLength = 255; // Extended Length value.
 
 private:
-    static Error Find(const Message &aMessage, uint8_t aType, uint16_t *aOffset, uint16_t *aSize, bool *aIsExtendedTlv);
+    struct ParsedInfo
+    {
+        Error ParseFrom(const Message &aMessage, uint16_t aOffset);
+        Error FindIn(const Message &aMessage, uint8_t aType);
+
+        uint8_t  mType;
+        uint16_t mLength;
+        uint16_t mOffset;
+        uint16_t mValueOffset;
+        uint16_t mSize;
+    };
+
     static Error FindTlv(const Message &aMessage, uint8_t aType, void *aValue, uint8_t aLength);
     static Error AppendTlv(Message &aMessage, uint8_t aType, const void *aValue, uint8_t aLength);
+    static Error ReadStringTlv(const Message &aMessage, uint16_t aOffset, uint8_t aMaxStringLength, char *aValue);
+    static Error FindStringTlv(const Message &aMessage, uint8_t aType, uint8_t aMaxStringLength, char *aValue);
+    static Error AppendStringTlv(Message &aMessage, uint8_t aType, uint8_t aMaxStringLength, const char *aValue);
     template <typename UintType> static Error ReadUintTlv(const Message &aMessage, uint16_t aOffset, UintType &aValue);
     template <typename UintType> static Error FindUintTlv(const Message &aMessage, uint8_t aType, UintType &aValue);
     template <typename UintType> static Error AppendUintTlv(Message &aMessage, uint8_t aType, UintType aValue);
@@ -477,10 +562,7 @@
  * @returns A `TlvType` pointer to `aTlv`.
  *
  */
-template <class TlvType> TlvType *As(Tlv *aTlv)
-{
-    return static_cast<TlvType *>(aTlv);
-}
+template <class TlvType> TlvType *As(Tlv *aTlv) { return static_cast<TlvType *>(aTlv); }
 
 /**
  * This template method casts a `Tlv` pointer to a given subclass `TlvType` pointer.
@@ -492,10 +574,7 @@
  * @returns A `TlvType` pointer to `aTlv`.
  *
  */
-template <class TlvType> const TlvType *As(const Tlv *aTlv)
-{
-    return static_cast<const TlvType *>(aTlv);
-}
+template <class TlvType> const TlvType *As(const Tlv *aTlv) { return static_cast<const TlvType *>(aTlv); }
 
 /**
  * This template method casts a `Tlv` reference to a given subclass `TlvType` reference.
@@ -507,10 +586,7 @@
  * @returns A `TlvType` reference to `aTlv`.
  *
  */
-template <class TlvType> TlvType &As(Tlv &aTlv)
-{
-    return static_cast<TlvType &>(aTlv);
-}
+template <class TlvType> TlvType &As(Tlv &aTlv) { return static_cast<TlvType &>(aTlv); }
 
 /**
  * This template method casts a `Tlv` reference to a given subclass `TlvType` reference.
@@ -522,10 +598,7 @@
  * @returns A `TlvType` reference to `aTlv`.
  *
  */
-template <class TlvType> const TlvType &As(const Tlv &aTlv)
-{
-    return static_cast<const TlvType &>(aTlv);
-}
+template <class TlvType> const TlvType &As(const Tlv &aTlv) { return static_cast<const TlvType &>(aTlv); }
 
 /**
  * This class defines constants for a TLV.
@@ -583,6 +656,24 @@
     typedef TlvValueType ValueType; ///< The TLV Value type.
 };
 
+/**
+ * This class defines constants and types for a simple TLV with a UTF-8 string value.
+ *
+ * This class and its sub-classes are intended to be used as the template type in `Tlv::Append<StringTlvType>()`,
+ * and the related `Tlv::Find<StringTlvType>()` and `Tlv::Read<StringTlvType>()`.
+ *
+ * @tparam kTlvTypeValue        The TLV Type value.
+ * @tparam kTlvMaxValueLength   The maximum allowed string length (as TLV value).
+ *
+ */
+template <uint8_t kTlvTypeValue, uint8_t kTlvMaxValueLength> class StringTlvInfo : public TlvInfo<kTlvTypeValue>
+{
+public:
+    static constexpr uint8_t kMaxStringLength = kTlvMaxValueLength; ///< Maximum string length.
+
+    typedef char StringType[kMaxStringLength + 1]; ///< String buffer for TLV value.
+};
+
 } // namespace ot
 
 #endif // TLVS_HPP_
diff --git a/src/core/common/trickle_timer.cpp b/src/core/common/trickle_timer.cpp
index bfa3aec..7258cd2 100644
--- a/src/core/common/trickle_timer.cpp
+++ b/src/core/common/trickle_timer.cpp
@@ -115,10 +115,7 @@
     TimerMilli::Start(mTimeInInterval);
 }
 
-void TrickleTimer::HandleTimer(Timer &aTimer)
-{
-    static_cast<TrickleTimer *>(&aTimer)->HandleTimer();
-}
+void TrickleTimer::HandleTimer(Timer &aTimer) { static_cast<TrickleTimer *>(&aTimer)->HandleTimer(); }
 
 void TrickleTimer::HandleTimer(void)
 {
@@ -161,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/type_traits.hpp b/src/core/common/type_traits.hpp
index 4feb52a..a608146 100644
--- a/src/core/common/type_traits.hpp
+++ b/src/core/common/type_traits.hpp
@@ -125,6 +125,41 @@
     typedef TypeOnTrue Type;
 };
 
+/**
+ * This type determines the return type of a given function pointer type.
+ *
+ * It provides member type named `Type` which gives the return type of `HandlerType` function pointer.
+ *
+ * For example, `ReturnTypeOf<Error (*)(void *aContext)>::Type` would be `Error`.
+ *
+ * @tparam HandlerType   The function pointer type.
+ *
+ */
+template <typename HandlerType> struct ReturnTypeOf;
+
+template <typename RetType, typename... Args> struct ReturnTypeOf<RetType (*)(Args...)>
+{
+    typedef RetType Type; ///< The return type.
+};
+
+/**
+ * This type determines the type of the first argument of a given function pointer type.
+ *
+ * It provides member type named `Type` which gives the first argument type of `HandlerType` function pointer.
+ *
+ * For example, `ReturnTypeOf<Error (*)(void *aContext)>::Type` would be `void *`.
+ *
+ * @tparam HandlerType   The function pointer type.
+ *
+ */
+template <typename HandlerType> struct FirstArgTypeOf;
+
+template <typename RetType, typename FirstArgType, typename... Args>
+struct FirstArgTypeOf<RetType (*)(FirstArgType, Args...)>
+{
+    typedef FirstArgType Type; ///< The first argument type.
+};
+
 } // namespace TypeTraits
 } // namespace ot
 
diff --git a/src/core/common/uptime.cpp b/src/core/common/uptime.cpp
index 1c3c74c..4cf0a34 100644
--- a/src/core/common/uptime.cpp
+++ b/src/core/common/uptime.cpp
@@ -46,7 +46,7 @@
     : InstanceLocator(aInstance)
     , mStartTime(TimerMilli::GetNow())
     , mOverflowCount(0)
-    , mTimer(aInstance, HandleTimer)
+    , mTimer(aInstance)
 {
     mTimer.FireAt(mStartTime + kTimerInterval);
 }
@@ -85,12 +85,7 @@
 {
     StringWriter writer(aBuffer, aSize);
 
-    UptimeToString(GetUptime(), writer);
-}
-
-void Uptime::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Uptime>().HandleTimer();
+    UptimeToString(GetUptime(), writer, /* aIncludeMsec */ true);
 }
 
 void Uptime::HandleTimer(void)
@@ -103,7 +98,7 @@
     mTimer.FireAt(mTimer.GetFireTime() + kTimerInterval);
 }
 
-static uint32_t DivideAndGetRemainder(uint32_t &aDividend, uint32_t aDivisor)
+static uint16_t DivideAndGetRemainder(uint32_t &aDividend, uint32_t aDivisor)
 {
     // Returns the quotient of division `aDividend / aDivisor` and updates
     // `aDividend` to returns the remainder
@@ -112,20 +107,20 @@
 
     aDividend -= quotient * aDivisor;
 
-    return quotient;
+    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;
-    uint32_t hours;
-    uint32_t minutes;
-    uint32_t seconds;
+    uint16_t hours;
+    uint16_t minutes;
+    uint16_t seconds;
 
     if (days > 0)
     {
-        aWriter.Append("%lud.", days);
+        aWriter.Append("%lud.", static_cast<unsigned long>(days));
         aUptime -= days * Time::kOneDayInMsec;
     }
 
@@ -134,7 +129,12 @@
     minutes   = DivideAndGetRemainder(remainder, Time::kOneMinuteInMsec);
     seconds   = DivideAndGetRemainder(remainder, Time::kOneSecondInMsec);
 
-    aWriter.Append("%02u:%02u:%02u.%03u", hours, minutes, seconds, 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 d6cab3c..670b023 100644
--- a/src/core/common/uptime.hpp
+++ b/src/core/common/uptime.hpp
@@ -90,25 +90,51 @@
      * 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);
 
     static_assert(static_cast<uint32_t>(4 * kTimerInterval) == 0, "kTimerInterval is not correct");
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTimer(void);
 
-    TimeMilli  mStartTime;
-    uint32_t   mOverflowCount;
-    TimerMilli mTimer;
+    using UptimeTimer = TimerMilliIn<Uptime, &Uptime::HandleTimer>;
+
+    TimeMilli   mStartTime;
+    uint32_t    mOverflowCount;
+    UptimeTimer mTimer;
 };
 
 } // namespace ot
diff --git a/examples/platforms/cc2538/openthread-core-cc2538-config-check.h b/src/core/config/border_agent.h
similarity index 65%
copy from examples/platforms/cc2538/openthread-core-cc2538-config-check.h
copy to src/core/config/border_agent.h
index 93788b1..7e0abc1 100644
--- a/examples/platforms/cc2538/openthread-core-cc2538-config-check.h
+++ b/src/core/config/border_agent.h
@@ -26,11 +26,43 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef OPENTHREAD_CORE_CC2538_CONFIG_CHECK_H_
-#define OPENTHREAD_CORE_CC2538_CONFIG_CHECK_H_
+/**
+ * @file
+ *   This file includes compile-time configurations for Border Agent.
+ *
+ */
 
-#if OPENTHREAD_CONFIG_RADIO_915MHZ_OQPSK_SUPPORT
-#error "Platform cc2538 doesn't support configuration option: OPENTHREAD_CONFIG_RADIO_915MHZ_OQPSK_SUPPORT"
+#ifndef CONFIG_BORDER_AGENT_H_
+#define CONFIG_BORDER_AGENT_H_
+
+/**
+ * @def OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
+ *
+ * Define to 1 to enable Border Agent support.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
+#define OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE 0
 #endif
 
-#endif /* OPENTHREAD_CORE_CC2538_CONFIG_CHECK_H_ */
+/**
+ * @def OPENTHREAD_CONFIG_BORDER_AGENT_UDP_PORT
+ *
+ * Specifies the Border Agent UDP port, and use 0 for ephemeral port.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_UDP_PORT
+#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/border_router.h b/src/core/config/border_router.h
index ec1792a..01ddfe9 100644
--- a/src/core/config/border_router.h
+++ b/src/core/config/border_router.h
@@ -36,16 +36,6 @@
 #define CONFIG_BORDER_ROUTER_H_
 
 /**
- * @def OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
- *
- * Define to 1 to enable Border Agent support.
- *
- */
-#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
-#define OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE 0
-#endif
-
-/**
  * @def OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
  *
  * Define to 1 to enable Border Router support.
@@ -81,65 +71,4 @@
 #define OPENTHREAD_CONFIG_BORDER_ROUTER_REQUEST_ROUTER_ROLE 1
 #endif
 
-/**
- * @def OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
- *
- * Define to 1 to enable Border Routing support.
- *
- */
-#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
-#define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 0
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_ROUTERS
- *
- * Specifies maximum number of routers (on infra link) to track by routing manager.
- *
- */
-#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_ROUTERS
-#define OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_ROUTERS 16
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_PREFIXES
- *
- * Specifies maximum number of discovered prefixes (on-link prefixes on the infra link) maintained by routing manager.
- *
- */
-#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_PREFIXES
-#define OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_PREFIXES 64
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_ON_MESH_PREFIXES
- *
- * Specified maximum number of on-mesh prefixes (discovered from Thread Network Data) that are included as Route Info
- * Option in emitted Router Advertisement messages.
- *
- */
-#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_ON_MESH_PREFIXES
-#define OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_ON_MESH_PREFIXES 16
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
- *
- * Define to 1 to enable Border Routing NAT64 support.
- *
- */
-#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
-#define OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE 0
-#endif
-
-/**
- * @def OPENTHREAD_CONFIG_BORDER_AGENT_UDP_PORT
- *
- * Specifies the Border Agent UDP port, and use 0 for ephemeral port.
- *
- */
-#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_UDP_PORT
-#define OPENTHREAD_CONFIG_BORDER_AGENT_UDP_PORT 0
-#endif
-
 #endif // CONFIG_BORDER_ROUTER_H_
diff --git a/src/core/config/border_routing.h b/src/core/config/border_routing.h
new file mode 100644
index 0000000..1327901
--- /dev/null
+++ b/src/core/config/border_routing.h
@@ -0,0 +1,107 @@
+/*
+ *  Copyright (c) 2021-22, 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 Border Routing Manager.
+ *
+ */
+
+#ifndef CONFIG_BORDER_ROUTING_H_
+#define CONFIG_BORDER_ROUTING_H_
+
+/**
+ * @def OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+ *
+ * Define to 1 to enable Border Routing Manager feature.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 0
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_ROUTERS
+ *
+ * Specifies maximum number of routers (on infra link) to track by routing manager.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_ROUTERS
+#define OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_ROUTERS 16
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_PREFIXES
+ *
+ * Specifies maximum number of discovered prefixes (on-link prefixes on the infra link) maintained by routing manager.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_PREFIXES
+#define OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_PREFIXES 64
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_ON_MESH_PREFIXES
+ *
+ * Specifies maximum number of on-mesh prefixes (discovered from Thread Network Data) that are included as Route Info
+ * Option in emitted Router Advertisement messages.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_ON_MESH_PREFIXES
+#define OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_ON_MESH_PREFIXES 16
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_OLD_ON_LINK_PREFIXES
+ *
+ * Specifies maximum number of old local on-link prefixes (being deprecated) maintained by routing manager.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_OLD_ON_LINK_PREFIXES
+#define OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_OLD_ON_LINK_PREFIXES 3
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_BORDER_ROUTING_ROUTER_ACTIVE_CHECK_TIMEOUT
+ *
+ * Specifies the timeout in msec for a discovered router on infra link side.
+ *
+ * This parameter is related to mechanism to check that a discovered router is still active.
+ *
+ * After this timeout elapses since the last received message (a Router or Neighbor Advertisement) from the router,
+ * routing manager will start sending Neighbor Solidification (NS) probes to the router to check that it is still
+ * active.
+ *
+ * This parameter can be considered to large value to practically disable this behavior.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_BORDER_ROUTING_ROUTER_ACTIVE_CHECK_TIMEOUT
+#define OPENTHREAD_CONFIG_BORDER_ROUTING_ROUTER_ACTIVE_CHECK_TIMEOUT (60 * 1000) // (in msec).
+#endif
+
+#endif // CONFIG_BORDER_ROUTING_H_
diff --git a/src/core/config/child_supervision.h b/src/core/config/child_supervision.h
index 282e5a0..94e8ec6 100644
--- a/src/core/config/child_supervision.h
+++ b/src/core/config/child_supervision.h
@@ -36,23 +36,11 @@
 #define CONFIG_CHILD_SUPERVISION_H_
 
 /**
- * @def OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
- *
- * Define to 1 to enable Child Supervision support.
- *
- */
-#ifndef OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-#define OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE 0
-#endif
-
-/**
  * @def OPENTHREAD_CONFIG_CHILD_SUPERVISION_INTERVAL
  *
- * The default supervision interval in seconds used by parent. Set to zero to disable the supervision process on the
- * parent.
+ * The default supervision interval in seconds to use when in child state. Zero indicates no supervision needed.
  *
- * Applicable only if child supervision feature is enabled (i.e.,
- * `OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE ` is set).
+ * The current supervision interval can be changed using `otChildSupervisionSetInterval()`.
  *
  * Child supervision feature provides a mechanism for parent to ensure that a message is sent to each sleepy child
  * within the supervision interval. If there is no transmission to the child within the supervision interval, child
@@ -69,7 +57,7 @@
  * The default supervision check timeout interval (in seconds) used by a device in child state. Set to zero to disable
  * the supervision check process on the child.
  *
- * Applicable only if child supervision feature is enabled (i.e., `OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE` is set).
+ * The check timeout interval can be changed using `otChildSupervisionSetCheckTimeout()`.
  *
  * If the sleepy child does not hear from its parent within the specified timeout interval, it initiates the re-attach
  * process (MLE Child Update Request/Response exchange with its parent).
@@ -80,15 +68,22 @@
 #endif
 
 /**
- * @def OPENTHREAD_CONFIG_CHILD_SUPERVISION_MSG_NO_ACK_REQUEST
+ * @def OPENTHREAD_CONFIG_CHILD_SUPERVISION_OLDER_VERSION_CHILD_DEFAULT_INTERVAL
  *
- * Define as 1 to clear/disable 15.4 ack request in the MAC header of a supervision message.
+ * Specifies the default supervision interval to use on parent for children that do not explicitly indicate their
+ * desired supervision internal (do not include a "Supervision Interval TLV") and are running older Thread versions
+ * (version <= 1.3.0).
  *
- * Applicable only if child supervision feature is enabled (i.e., `OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE` is set).
+ * This config is added to allow backward compatibility on parent with SED children that used Child Supervision
+ * feature in OT stack before adoption of it by Thread specification and addition of the "Supervision Interval TLV" as
+ * the mechanism for child to inform the parent of its desired supervision interval.
+ *
+ * The config can be set to zero to effectively disable it, i.e., if a child does not provide "Supervision Interval TLV"
+ * it indicates that it does not want to be supervised and then parent will use zero interval for the child.
  *
  */
-#ifndef OPENTHREAD_CONFIG_CHILD_SUPERVISION_MSG_NO_ACK_REQUEST
-#define OPENTHREAD_CONFIG_CHILD_SUPERVISION_MSG_NO_ACK_REQUEST 0
+#ifndef OPENTHREAD_CONFIG_CHILD_SUPERVISION_OLDER_VERSION_CHILD_DEFAULT_INTERVAL
+#define OPENTHREAD_CONFIG_CHILD_SUPERVISION_OLDER_VERSION_CHILD_DEFAULT_INTERVAL 129
 #endif
 
 #endif // CONFIG_CHILD_SUPERVISION_H_
diff --git a/src/core/config/commissioner.h b/src/core/config/commissioner.h
index a875d8d..ff43adb 100644
--- a/src/core/config/commissioner.h
+++ b/src/core/config/commissioner.h
@@ -55,4 +55,15 @@
 #define OPENTHREAD_CONFIG_COMMISSIONER_MAX_JOINER_ENTRIES 2
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_COMMISSIONER_JOINER_SESSION_TIMEOUT
+ *
+ * The timeout for the Joiner's session, in seconds. After this timeout,
+ * the Commissioner tears down the session.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_COMMISSIONER_JOINER_SESSION_TIMEOUT
+#define OPENTHREAD_CONFIG_COMMISSIONER_JOINER_SESSION_TIMEOUT 30
+#endif
+
 #endif // CONFIG_COMMISSIONER_H_
diff --git a/src/core/config/dns_client.h b/src/core/config/dns_client.h
index e0a9ec8..d727261 100644
--- a/src/core/config/dns_client.h
+++ b/src/core/config/dns_client.h
@@ -35,6 +35,7 @@
 #ifndef CONFIG_DNS_CLIENT_H_
 #define CONFIG_DNS_CLIENT_H_
 
+#include "config/ip6.h"
 #include "config/srp_client.h"
 
 /**
@@ -150,4 +151,34 @@
 #define OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_RECURSION_DESIRED_FLAG 1
 #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.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+#define OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE 0
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_QUERY_MAX_SIZE
+ *
+ * Specifies size of receive and transmit buffers of TCP sockets for DNS query over TCP.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_QUERY_MAX_SIZE
+#define OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_QUERY_MAX_SIZE 1024
+#endif
+
 #endif // CONFIG_DNS_CLIENT_H_
diff --git a/src/core/config/dnssd_server.h b/src/core/config/dnssd_server.h
index 7083493..3edc409 100644
--- a/src/core/config/dnssd_server.h
+++ b/src/core/config/dnssd_server.h
@@ -75,4 +75,15 @@
 #define OPENTHREAD_CONFIG_DNSSD_QUERY_TIMEOUT 6000
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+ *
+ * Define to 1 to enable upstream forwarding support. The platform MUST implement `otPlatDnsStartUpstreamQuery` and
+ * `otPlatDnsCancelUpstreamQuery`.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+#define OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE 0
+#endif
+
 #endif // CONFIG_DNSSD_SERVER_H_
diff --git a/src/core/config/dtls.h b/src/core/config/dtls.h
index be6077d..09e8337 100644
--- a/src/core/config/dtls.h
+++ b/src/core/config/dtls.h
@@ -35,7 +35,7 @@
 #ifndef CONFIG_DTLS_H_
 #define CONFIG_DTLS_H_
 
-#include "config/border_router.h"
+#include "config/border_agent.h"
 #include "config/coap.h"
 #include "config/commissioner.h"
 #include "config/joiner.h"
@@ -50,11 +50,16 @@
 #define OPENTHREAD_CONFIG_DTLS_MAX_CONTENT_LEN MBEDTLS_SSL_IN_CONTENT_LEN
 #endif
 
-#if OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE || OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE || \
-    OPENTHREAD_CONFIG_COMMISSIONER_ENABLE || OPENTHREAD_CONFIG_JOINER_ENABLE
-#define OPENTHREAD_CONFIG_DTLS_ENABLE 1
-#else
-#define OPENTHREAD_CONFIG_DTLS_ENABLE 0
+/**
+ * @def OPENTHREAD_CONFIG_DTLS_ENABLE
+ *
+ *  Define to 1 to enable DTLS.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_DTLS_ENABLE
+#define OPENTHREAD_CONFIG_DTLS_ENABLE                                                     \
+    (OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE || OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE || \
+     OPENTHREAD_CONFIG_COMMISSIONER_ENABLE || OPENTHREAD_CONFIG_JOINER_ENABLE)
 #endif
 
 #endif // CONFIG_DTLS_H_
diff --git a/src/core/config/history_tracker.h b/src/core/config/history_tracker.h
index 32da285..591474a 100644
--- a/src/core/config/history_tracker.h
+++ b/src/core/config/history_tracker.h
@@ -128,6 +128,18 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_ROUTER_LIST_SIZE
+ *
+ * Specifies the maximum number of entries in router table history list.
+ *
+ * Can be set to zero to configure History Tracker module not to collect any router table history.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_HISTORY_TRACKER_ROUTER_LIST_SIZE
+#define OPENTHREAD_CONFIG_HISTORY_TRACKER_ROUTER_LIST_SIZE 256
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_HISTORY_TRACKER_ON_MESH_PREFIX_LIST_SIZE
  *
  * Specifies the maximum number of entries in Network Data On Mesh Prefix history list.
diff --git a/src/core/config/ip6.h b/src/core/config/ip6.h
index b84918f..75976e1 100644
--- a/src/core/config/ip6.h
+++ b/src/core/config/ip6.h
@@ -35,6 +35,8 @@
 #ifndef CONFIG_IP6_H_
 #define CONFIG_IP6_H_
 
+#include "config/border_routing.h"
+
 /**
  * @def OPENTHREAD_CONFIG_IP6_MAX_EXT_UCAST_ADDRS
  *
@@ -134,7 +136,7 @@
  *
  */
 #ifndef OPENTHREAD_CONFIG_MPL_SEED_SET_ENTRIES
-#define OPENTHREAD_CONFIG_MPL_SEED_SET_ENTRIES 32
+#define OPENTHREAD_CONFIG_MPL_SEED_SET_ENTRIES 35
 #endif
 
 /**
@@ -171,6 +173,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_TLS_ENABLE
+ *
+ * Define as 1 to enable support for TLS over TCP.
+ *
+ */
+#if OPENTHREAD_CONFIG_TCP_ENABLE && !defined(OPENTHREAD_CONFIG_TLS_ENABLE)
+#define OPENTHREAD_CONFIG_TLS_ENABLE 1
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_IP6_ALLOW_LOOP_BACK_HOST_DATAGRAMS
  *
  * Define as 1 to allow IPv6 datagrams from Host to be looped back to Host.
@@ -180,4 +192,14 @@
 #define OPENTHREAD_CONFIG_IP6_ALLOW_LOOP_BACK_HOST_DATAGRAMS 1
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+ *
+ * Define as 1 to enable IPv6 Border Routing counters.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+#define OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#endif
+
 #endif // CONFIG_IP6_H_
diff --git a/src/core/config/mac.h b/src/core/config/mac.h
index d44ff11..0358b22 100644
--- a/src/core/config/mac.h
+++ b/src/core/config/mac.h
@@ -467,6 +467,18 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_CSL_TRANSMIT_TIME_AHEAD
+ *
+ * Transmission scheduling and ramp up time needed for the CSL transmitter to be ready, in units of microseconds.
+ * This time must include at least the radio's turnaround time between end of CCA and start of preamble transmission.
+ * To avoid early CSL transmission it also must not be configured higher than the actual scheduling and ramp up time.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_CSL_TRANSMIT_TIME_AHEAD
+#define OPENTHREAD_CONFIG_CSL_TRANSMIT_TIME_AHEAD 40
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_CSL_RECEIVE_TIME_AHEAD
  *
  * Reception scheduling and ramp up time needed for the CSL receiver to be ready, in units of microseconds.
@@ -521,4 +533,15 @@
 #define OPENTHREAD_CONFIG_MAC_OUTGOING_BEACON_PAYLOAD_ENABLE 0
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_MAC_DATA_POLL_TIMEOUT
+ *
+ * This setting specifies the timeout for receiving the Data Frame (in msec) - after an ACK with FP bit set was
+ * received.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MAC_DATA_POLL_TIMEOUT
+#define OPENTHREAD_CONFIG_MAC_DATA_POLL_TIMEOUT 100
+#endif
+
 #endif // CONFIG_MAC_H_
diff --git a/examples/platforms/cc2538/system.c b/src/core/config/mesh_diag.h
similarity index 64%
copy from examples/platforms/cc2538/system.c
copy to src/core/config/mesh_diag.h
index 0d1cd63..4049255 100644
--- a/examples/platforms/cc2538/system.c
+++ b/src/core/config/mesh_diag.h
@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2016, The OpenThread Authors.
+ *  Copyright (c) 2023, The OpenThread Authors.
  *  All rights reserved.
  *
  *  Redistribution and use in source and binary forms, with or without
@@ -28,39 +28,35 @@
 
 /**
  * @file
- * @brief
- *   This file includes the platform-specific initializers.
+ *   This file includes compile-time configurations for Mesh Diagnostic module.
+ *
  */
-#include "platform-cc2538.h"
-#include <openthread/config.h>
 
-otInstance *sInstance;
+#ifndef CONFIG_MESH_DIAG_H_
+#define CONFIG_MESH_DIAG_H_
 
-void otSysInit(int argc, char *argv[])
-{
-    OT_UNUSED_VARIABLE(argc);
-    OT_UNUSED_VARIABLE(argv);
+#include "config/border_routing.h"
 
-#if OPENTHREAD_CONFIG_ENABLE_DEBUG_UART
-    cc2538DebugUartInit();
+/**
+ * @def OPENTHREAD_CONFIG_MESH_DIAG_ENABLE
+ *
+ * Define to 1 to enable Mesh Diagnostic module.
+ *
+ * By default this feature is enabled if device is configured to act as Border Router.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MESH_DIAG_ENABLE
+#define OPENTHREAD_CONFIG_MESH_DIAG_ENABLE OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 #endif
-    cc2538AlarmInit();
-    cc2538RandomInit();
-    cc2538RadioInit();
-}
 
-bool otSysPseudoResetWasRequested(void)
-{
-    return false;
-}
+/**
+ * @def OPENTHREAD_CONFIG_MESH_DIAG_RESPONSE_TIMEOUT
+ *
+ * Specifies the timeout interval in milliseconds waiting for response from router during discover.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MESH_DIAG_RESPONSE_TIMEOUT
+#define OPENTHREAD_CONFIG_MESH_DIAG_RESPONSE_TIMEOUT 5000
+#endif
 
-void otSysProcessDrivers(otInstance *aInstance)
-{
-    sInstance = aInstance;
-
-    // should sleep and wait for interrupts here
-
-    cc2538UartProcess();
-    cc2538RadioProcess(aInstance);
-    cc2538AlarmProcess(aInstance);
-}
+#endif // CONFIG_MESH_DIAG_H_
diff --git a/src/core/config/misc.h b/src/core/config/misc.h
index 3070bb0..c857850 100644
--- a/src/core/config/misc.h
+++ b/src/core/config/misc.h
@@ -78,6 +78,19 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_DEVICE_POWER_SUPPLY
+ *
+ * Specifies the default device power supply config. This config MUST use values from `otPowerSupply` enumeration.
+ *
+ * Device manufacturer can use this config to set the power supply config used by the device. This is then used as part
+ * of default `otDeviceProperties` to determine the Leader Weight used by the device.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_DEVICE_POWER_SUPPLY
+#define OPENTHREAD_CONFIG_DEVICE_POWER_SUPPLY OT_POWER_SUPPLY_EXTERNAL
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_ECDSA_ENABLE
  *
  * Define to 1 to enable ECDSA support.
@@ -88,13 +101,24 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_DETERMINISTIC_ECDSA_ENABLE
+ *
+ * Define to 1 to generate ECDSA signatures deterministically
+ * according to RFC 6979 instead of randomly.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_DETERMINISTIC_ECDSA_ENABLE
+#define OPENTHREAD_CONFIG_DETERMINISTIC_ECDSA_ENABLE 1
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_UPTIME_ENABLE
  *
  * Define to 1 to enable tracking the uptime of OpenThread instance.
  *
  */
 #ifndef OPENTHREAD_CONFIG_UPTIME_ENABLE
-#define OPENTHREAD_CONFIG_UPTIME_ENABLE 0
+#define OPENTHREAD_CONFIG_UPTIME_ENABLE OPENTHREAD_FTD
 #endif
 
 /**
@@ -170,6 +194,9 @@
  * 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 requirements, including
+ * OPENTHREAD_CONFIG_MLE_MAX_CHILDREN and OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE.
+ *
  */
 #ifndef OPENTHREAD_CONFIG_MESSAGE_BUFFER_SIZE
 #define OPENTHREAD_CONFIG_MESSAGE_BUFFER_SIZE (sizeof(void *) * 32)
@@ -244,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.
  *
  */
@@ -257,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
@@ -333,6 +360,19 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_ASSERT_CHECK_API_POINTER_PARAM_FOR_NULL
+ *
+ * Define as 1 to enable assert check of pointer-type API input parameters against null.
+ *
+ * Enabling this feature can increase code-size significantly due to many assert checks added for all API pointer
+ * parameters. It is recommended to enable and use this feature during debugging only.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_ASSERT_CHECK_API_POINTER_PARAM_FOR_NULL
+#define OPENTHREAD_CONFIG_ASSERT_CHECK_API_POINTER_PARAM_FOR_NULL 0
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_ENABLE_DEBUG_UART
  *
  * Enable the "Debug Uart" platform feature.
@@ -555,16 +595,6 @@
 #endif // OPENTHREAD_CONFIG_DEFAULT_CHANNEL
 
 /**
- * @def OPENTHREAD_CONFIG_LEGACY_ENABLE
- *
- * Define to 1 to enable legacy network support.
- *
- */
-#ifndef OPENTHREAD_CONFIG_LEGACY_ENABLE
-#define OPENTHREAD_CONFIG_LEGACY_ENABLE 0
-#endif
-
-/**
  * @def OPENTHREAD_CONFIG_OTNS_ENABLE
  *
  * Define to 1 to enable OTNS interactions.
@@ -604,4 +634,14 @@
 #define OPENTHREAD_CONFIG_NEIGHBOR_DISCOVERY_AGENT_ENABLE 0
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_ALLOW_EMPTY_NETWORK_NAME
+ *
+ * Define as 1 to enable support for an empty network name (zero-length: "")
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_ALLOW_EMPTY_NETWORK_NAME
+#define OPENTHREAD_CONFIG_ALLOW_EMPTY_NETWORK_NAME 0
+#endif
+
 #endif // CONFIG_MISC_H_
diff --git a/src/core/config/mle.h b/src/core/config/mle.h
index 872e1a7..57e1e50 100644
--- a/src/core/config/mle.h
+++ b/src/core/config/mle.h
@@ -89,6 +89,18 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_MLE_DEFAULT_LEADER_WEIGHT_ADJUSTMENT
+ *
+ * Specifies the default value for `mLeaderWeightAdjustment` in `otDeviceProperties`. MUST be from -16 up to +16.
+ *
+ * This value is used to adjust the calculated Leader Weight from `otDeviceProperties`.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MLE_DEFAULT_LEADER_WEIGHT_ADJUSTMENT
+#define OPENTHREAD_CONFIG_MLE_DEFAULT_LEADER_WEIGHT_ADJUSTMENT 0
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
  *
  * Enable setting steering data out of band.
@@ -257,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/nat64.h b/src/core/config/nat64.h
new file mode 100644
index 0000000..fff5883
--- /dev/null
+++ b/src/core/config/nat64.h
@@ -0,0 +1,78 @@
+/*
+ *  Copyright (c) 2022, 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 NAT64.
+ *
+ */
+
+#ifndef CONFIG_NAT64_H_
+#define CONFIG_NAT64_H_
+
+/**
+ * @def OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+ *
+ * Define to 1 to enable the internal NAT64 translator.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+#define OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE 0
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_NAT64_MAX_MAPPINGS
+ *
+ * Specifies maximum number of active mappings for NAT64.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NAT64_MAX_MAPPINGS
+#define OPENTHREAD_CONFIG_NAT64_MAX_MAPPINGS 254
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_NAT64_IDLE_TIMEOUT_SECONDS
+ *
+ * Specifies timeout in seconds before removing an inactive address mapping.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NAT64_IDLE_TIMEOUT_SECONDS
+#define OPENTHREAD_CONFIG_NAT64_IDLE_TIMEOUT_SECONDS 7200
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+ *
+ * Define to 1 to enable NAT64 support in Border Routing Manager.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+#define OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE 0
+#endif
+
+#endif
diff --git a/src/core/config/netdata_publisher.h b/src/core/config/netdata_publisher.h
index bd3ef8f..ee8fe58 100644
--- a/src/core/config/netdata_publisher.h
+++ b/src/core/config/netdata_publisher.h
@@ -36,6 +36,7 @@
 #define CONFIG_NETDATA_PUBLISHER_H_
 
 #include "config/border_router.h"
+#include "config/border_routing.h"
 #include "config/srp_server.h"
 
 /**
@@ -147,17 +148,12 @@
 /**
  * @def OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_PREFIX_ENTRIES
  *
- * Specifies maximum number of prefix (on-mesh prefix or external route) entries supported by Publisher.
+ * Specifies maximum number of prefix (on-mesh prefix or external route) entries reserved by Publisher for use by
+ * user (through OT public APIs).
  *
  */
 #ifndef OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_PREFIX_ENTRIES
-
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
-#define OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_PREFIX_ENTRIES \
-    (OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_PREFIXES + 5)
-#else
 #define OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_PREFIX_ENTRIES 3
 #endif
-#endif
 
 #endif // CONFIG_NETDATA_PUBLISHER_H_
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 4cf7121..67976e1 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
@@ -145,6 +149,10 @@
 #error "OPENTHREAD_ENABLE_LEGACY was replaced by OPENTHREAD_CONFIG_LEGACY_ENABLE."
 #endif
 
+#ifdef OPENTHREAD_CONFIG_LEGACY_ENABLE
+#error "OPENTHREAD_CONFIG_LEGACY_ENABLE was removed."
+#endif
+
 #ifdef OPENTHREAD_ENABLE_CHILD_SUPERVISION
 #error "OPENTHREAD_ENABLE_CHILD_SUPERVISION was replaced by OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE."
 #endif
@@ -501,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
@@ -632,4 +640,32 @@
         "OPENTHREAD_CONFIG_SRP_CLIENT_UPDATE_TX_MIN_DELAY and OPENTHREAD_CONFIG_SRP_CLIENT_UPDATE_TX_MAX_DELAY"
 #endif
 
+#ifdef OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE
+#error "OPENTHREAD_CONFIG_BORDER_ROUTING_NAT64_ENABLE was replaced by OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE"
+#endif
+
+#ifdef OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
+#error "OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE is removed. The feature is now supported by default (on 1.3.0)"
+#endif
+
+#ifdef OPENTHREAD_CONFIG_CHILD_SUPERVISION_MSG_NO_ACK_REQUEST
+#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
+
 #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/platform.h b/src/core/config/platform.h
index ca132db..9fa475b 100644
--- a/src/core/config/platform.h
+++ b/src/core/config/platform.h
@@ -158,6 +158,16 @@
 #define OPENTHREAD_CONFIG_PLATFORM_MAC_KEYS_EXPORTABLE_ENABLE 0
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+ *
+ * Define as 1 to enable platform power calibration support.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+#define OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE 0
+#endif
+
 #if OPENTHREAD_CONFIG_PLATFORM_RADIO_PROPRIETARY_SUPPORT
 #if (!defined(OPENTHREAD_CONFIG_PLATFORM_RADIO_PROPRIETARY_CHANNEL_PAGE) || \
      !defined(OPENTHREAD_CONFIG_PLATFORM_RADIO_PROPRIETARY_CHANNEL_MIN) ||  \
diff --git a/src/core/config/power_calibration.h b/src/core/config/power_calibration.h
new file mode 100644
index 0000000..56e9467
--- /dev/null
+++ b/src/core/config/power_calibration.h
@@ -0,0 +1,68 @@
+/*
+ *  Copyright (c) 2022, 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 power calibration module.
+ *
+ */
+
+#ifndef CONFIG_POWER_CALIBRATION_H_
+#define CONFIG_POWER_CALIBRATION_H_
+
+/**
+ * @def OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE
+ *
+ * Define as 1 to enable power calibration support.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE
+#define OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE 0
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_POWER_CALIBRATION_RAW_POWER_SETTING_SIZE
+ *
+ * The size of the raw power setting byte array.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_POWER_CALIBRATION_RAW_POWER_SETTING_SIZE
+#define OPENTHREAD_CONFIG_POWER_CALIBRATION_RAW_POWER_SETTING_SIZE 16
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_POWER_CALIBRATION_NUM_CALIBRATED_POWER_ENTRIES
+ *
+ * The number of the calibrated power entries for each channel.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_POWER_CALIBRATION_NUM_CALIBRATED_POWER_ENTRIES
+#define OPENTHREAD_CONFIG_POWER_CALIBRATION_NUM_CALIBRATED_POWER_ENTRIES 6
+#endif
+
+#endif // CONFIG_POWER_CALIBRATION_H_
diff --git a/src/core/config/srp_client.h b/src/core/config/srp_client.h
index 4bef258..ca609ab 100644
--- a/src/core/config/srp_client.h
+++ b/src/core/config/srp_client.h
@@ -35,6 +35,8 @@
 #ifndef CONFIG_SRP_CLIENT_H_
 #define CONFIG_SRP_CLIENT_H_
 
+#include "config/misc.h"
+
 /**
  * @def OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
  *
@@ -332,8 +334,12 @@
  *
  */
 #ifndef OPENTHREAD_CONFIG_SRP_CLIENT_BUFFERS_MAX_HOST_ADDRESSES
+#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+#define OPENTHREAD_CONFIG_SRP_CLIENT_BUFFERS_MAX_HOST_ADDRESSES 10
+#else
 #define OPENTHREAD_CONFIG_SRP_CLIENT_BUFFERS_MAX_HOST_ADDRESSES 2
 #endif
+#endif
 
 /**
  * @def OPENTHREAD_CONFIG_SRP_CLIENT_BUFFERS_HOST_NAME_SIZE
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 538ccaf..653ed75 100644
--- a/src/core/config/tmf.h
+++ b/src/core/config/tmf.h
@@ -42,7 +42,7 @@
  *
  */
 #ifndef OPENTHREAD_CONFIG_TMF_ADDRESS_CACHE_ENTRIES
-#define OPENTHREAD_CONFIG_TMF_ADDRESS_CACHE_ENTRIES 10
+#define OPENTHREAD_CONFIG_TMF_ADDRESS_CACHE_ENTRIES 32
 #endif
 
 /**
@@ -107,6 +107,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_TMF_ALLOW_ADDRESS_RESOLUTION_USING_NET_DATA_SERVICES
+ *
+ * Define as 1 to allow address resolution of on-mesh addresses using Thread Network Data DNS/SRP Service entries.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_TMF_ALLOW_ADDRESS_RESOLUTION_USING_NET_DATA_SERVICES
+#define OPENTHREAD_CONFIG_TMF_ALLOW_ADDRESS_RESOLUTION_USING_NET_DATA_SERVICES 1
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_TMF_PENDING_DATASET_MINIMUM_DELAY
  *
  * Minimum Delay Timer value for a Pending Operational Dataset (in ms).
@@ -163,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
 
 /**
@@ -215,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/aes_ccm.cpp b/src/core/crypto/aes_ccm.cpp
index 49c42f2..bb9bec1 100644
--- a/src/core/crypto/aes_ccm.cpp
+++ b/src/core/crypto/aes_ccm.cpp
@@ -280,7 +280,7 @@
 void AesCcm::GenerateNonce(const Mac::ExtAddress &aAddress,
                            uint32_t               aFrameCounter,
                            uint8_t                aSecurityLevel,
-                           uint8_t *              aNonce)
+                           uint8_t               *aNonce)
 {
     memcpy(aNonce, aAddress.m8, sizeof(Mac::ExtAddress));
     aNonce += sizeof(Mac::ExtAddress);
diff --git a/src/core/crypto/aes_ccm.hpp b/src/core/crypto/aes_ccm.hpp
index 0234243..8ae9ccc 100644
--- a/src/core/crypto/aes_ccm.hpp
+++ b/src/core/crypto/aes_ccm.hpp
@@ -196,7 +196,7 @@
     static void GenerateNonce(const Mac::ExtAddress &aAddress,
                               uint32_t               aFrameCounter,
                               uint8_t                aSecurityLevel,
-                              uint8_t *              aNonce);
+                              uint8_t               *aNonce);
 
 private:
     AesEcb   mEcb;
diff --git a/src/core/crypto/aes_ecb.cpp b/src/core/crypto/aes_ecb.cpp
index f13e050..70966e5 100644
--- a/src/core/crypto/aes_ecb.cpp
+++ b/src/core/crypto/aes_ecb.cpp
@@ -45,20 +45,14 @@
     SuccessOrAssert(otPlatCryptoAesInit(&mContext));
 }
 
-void AesEcb::SetKey(const Key &aKey)
-{
-    SuccessOrAssert(otPlatCryptoAesSetKey(&mContext, &aKey));
-}
+void AesEcb::SetKey(const Key &aKey) { SuccessOrAssert(otPlatCryptoAesSetKey(&mContext, &aKey)); }
 
 void AesEcb::Encrypt(const uint8_t aInput[kBlockSize], uint8_t aOutput[kBlockSize])
 {
     SuccessOrAssert(otPlatCryptoAesEncrypt(&mContext, aInput, aOutput));
 }
 
-AesEcb::~AesEcb(void)
-{
-    SuccessOrAssert(otPlatCryptoAesFree(&mContext));
-}
+AesEcb::~AesEcb(void) { SuccessOrAssert(otPlatCryptoAesFree(&mContext)); }
 
 } // namespace Crypto
 } // namespace ot
diff --git a/src/core/crypto/aes_ecb.hpp b/src/core/crypto/aes_ecb.hpp
index 3e30f5f..61680ad 100644
--- a/src/core/crypto/aes_ecb.hpp
+++ b/src/core/crypto/aes_ecb.hpp
@@ -62,13 +62,13 @@
     static constexpr uint8_t kBlockSize = 16; ///< AES-128 block size (bytes).
 
     /**
-     * Constructor to initialize the mbedtls_aes_context.
+     * Constructor to initialize the AES operation.
      *
      */
     AesEcb(void);
 
     /**
-     * Destructor to free the mbedtls_aes_context.
+     * Destructor to free the AES context.
      *
      */
     ~AesEcb(void);
diff --git a/src/core/crypto/crypto_platform.cpp b/src/core/crypto/crypto_platform.cpp
index 7b3a770..a8da82e 100644
--- a/src/core/crypto/crypto_platform.cpp
+++ b/src/core/crypto/crypto_platform.cpp
@@ -32,11 +32,17 @@
 
 #include "openthread-core-config.h"
 
+#include <string.h>
+
 #include <mbedtls/aes.h>
+#include <mbedtls/cmac.h>
 #include <mbedtls/ctr_drbg.h>
+#include <mbedtls/ecdsa.h>
 #include <mbedtls/entropy.h>
 #include <mbedtls/md.h>
+#include <mbedtls/pk.h>
 #include <mbedtls/sha256.h>
+#include <mbedtls/version.h>
 
 #include <openthread/instance.h>
 #include <openthread/platform/crypto.h>
@@ -48,6 +54,7 @@
 #include "common/instance.hpp"
 #include "common/new.hpp"
 #include "config/crypto.h"
+#include "crypto/ecdsa.hpp"
 #include "crypto/hmac_sha256.hpp"
 #include "crypto/storage.hpp"
 
@@ -147,7 +154,7 @@
 {
     Error                    error  = kErrorNone;
     const mbedtls_md_info_t *mdInfo = nullptr;
-    mbedtls_md_context_t *   context;
+    mbedtls_md_context_t    *context;
 
     VerifyOrExit(aContext != nullptr, error = kErrorInvalidArgs);
     VerifyOrExit(aContext->mContextSize >= sizeof(mbedtls_md_context_t), error = kErrorFailed);
@@ -239,9 +246,9 @@
 }
 
 OT_TOOL_WEAK otError otPlatCryptoHkdfExpand(otCryptoContext *aContext,
-                                            const uint8_t *  aInfo,
+                                            const uint8_t   *aInfo,
                                             uint16_t         aInfoLength,
-                                            uint8_t *        aOutputKey,
+                                            uint8_t         *aOutputKey,
                                             uint16_t         aOutputKeyLength)
 {
     Error             error = kErrorNone;
@@ -287,7 +294,7 @@
         hmac.Update(iter);
         hmac.Finish(hash);
 
-        copyLength = (aOutputKeyLength > sizeof(hash)) ? sizeof(hash) : aOutputKeyLength;
+        copyLength = Min(aOutputKeyLength, static_cast<uint16_t>(sizeof(hash)));
 
         memcpy(aOutputKey, hash.GetBytes(), copyLength);
         aOutputKey += copyLength;
@@ -298,8 +305,8 @@
     return error;
 }
 
-OT_TOOL_WEAK otError otPlatCryptoHkdfExtract(otCryptoContext *  aContext,
-                                             const uint8_t *    aSalt,
+OT_TOOL_WEAK otError otPlatCryptoHkdfExtract(otCryptoContext   *aContext,
+                                             const uint8_t     *aSalt,
                                              uint16_t           aSaltLength,
                                              const otCryptoKey *aInputKey)
 {
@@ -484,6 +491,300 @@
         mbedtls_ctr_drbg_random(&sCtrDrbgContext, static_cast<unsigned char *>(aBuffer), static_cast<size_t>(aSize)));
 }
 
+#if OPENTHREAD_CONFIG_ECDSA_ENABLE
+
+OT_TOOL_WEAK otError otPlatCryptoEcdsaGenerateKey(otPlatCryptoEcdsaKeyPair *aKeyPair)
+{
+    mbedtls_pk_context pk;
+    int                ret;
+
+    mbedtls_pk_init(&pk);
+
+    ret = mbedtls_pk_setup(&pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY));
+    VerifyOrExit(ret == 0);
+
+    ret = mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, mbedtls_pk_ec(pk), MbedTls::CryptoSecurePrng, nullptr);
+    VerifyOrExit(ret == 0);
+
+    ret = mbedtls_pk_write_key_der(&pk, aKeyPair->mDerBytes, OT_CRYPTO_ECDSA_MAX_DER_SIZE);
+    VerifyOrExit(ret > 0);
+
+    aKeyPair->mDerLength = static_cast<uint8_t>(ret);
+
+    memmove(aKeyPair->mDerBytes, aKeyPair->mDerBytes + OT_CRYPTO_ECDSA_MAX_DER_SIZE - aKeyPair->mDerLength,
+            aKeyPair->mDerLength);
+
+exit:
+    mbedtls_pk_free(&pk);
+
+    return (ret >= 0) ? kErrorNone : MbedTls::MapError(ret);
+}
+
+OT_TOOL_WEAK otError otPlatCryptoEcdsaGetPublicKey(const otPlatCryptoEcdsaKeyPair *aKeyPair,
+                                                   otPlatCryptoEcdsaPublicKey     *aPublicKey)
+{
+    Error                error = kErrorNone;
+    mbedtls_pk_context   pk;
+    mbedtls_ecp_keypair *keyPair;
+    int                  ret;
+
+    mbedtls_pk_init(&pk);
+
+    VerifyOrExit(mbedtls_pk_setup(&pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0, error = kErrorFailed);
+
+#if (MBEDTLS_VERSION_NUMBER >= 0x03000000)
+    VerifyOrExit(mbedtls_pk_parse_key(&pk, aKeyPair->mDerBytes, aKeyPair->mDerLength, nullptr, 0,
+                                      MbedTls::CryptoSecurePrng, nullptr) == 0,
+                 error = kErrorParse);
+#else
+    VerifyOrExit(mbedtls_pk_parse_key(&pk, aKeyPair->mDerBytes, aKeyPair->mDerLength, nullptr, 0) == 0,
+                 error = kErrorParse);
+#endif
+
+    keyPair = mbedtls_pk_ec(pk);
+
+    ret = mbedtls_mpi_write_binary(&keyPair->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), aPublicKey->m8,
+                                   Ecdsa::P256::kMpiSize);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+
+    ret = mbedtls_mpi_write_binary(&keyPair->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y),
+                                   aPublicKey->m8 + Ecdsa::P256::kMpiSize, Ecdsa::P256::kMpiSize);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+
+exit:
+    mbedtls_pk_free(&pk);
+    return error;
+}
+
+OT_TOOL_WEAK otError otPlatCryptoEcdsaSign(const otPlatCryptoEcdsaKeyPair *aKeyPair,
+                                           const otPlatCryptoSha256Hash   *aHash,
+                                           otPlatCryptoEcdsaSignature     *aSignature)
+{
+    Error                 error = kErrorNone;
+    mbedtls_pk_context    pk;
+    mbedtls_ecp_keypair  *keypair;
+    mbedtls_ecdsa_context ecdsa;
+    mbedtls_mpi           r;
+    mbedtls_mpi           s;
+    int                   ret;
+
+    mbedtls_pk_init(&pk);
+    mbedtls_ecdsa_init(&ecdsa);
+    mbedtls_mpi_init(&r);
+    mbedtls_mpi_init(&s);
+
+    VerifyOrExit(mbedtls_pk_setup(&pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0, error = kErrorFailed);
+
+#if (MBEDTLS_VERSION_NUMBER >= 0x03000000)
+    VerifyOrExit(mbedtls_pk_parse_key(&pk, aKeyPair->mDerBytes, aKeyPair->mDerLength, nullptr, 0,
+                                      MbedTls::CryptoSecurePrng, nullptr) == 0,
+                 error = kErrorParse);
+#else
+    VerifyOrExit(mbedtls_pk_parse_key(&pk, aKeyPair->mDerBytes, aKeyPair->mDerLength, nullptr, 0) == 0,
+                 error = kErrorParse);
+#endif
+
+    keypair = mbedtls_pk_ec(pk);
+
+    ret = mbedtls_ecdsa_from_keypair(&ecdsa, keypair);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+
+#if (MBEDTLS_VERSION_NUMBER >= 0x02130000)
+    ret = mbedtls_ecdsa_sign_det_ext(&ecdsa.MBEDTLS_PRIVATE(grp), &r, &s, &ecdsa.MBEDTLS_PRIVATE(d), aHash->m8,
+                                     Sha256::Hash::kSize, MBEDTLS_MD_SHA256, MbedTls::CryptoSecurePrng, nullptr);
+#else
+    ret = mbedtls_ecdsa_sign_det(&ecdsa.MBEDTLS_PRIVATE(grp), &r, &s, &ecdsa.MBEDTLS_PRIVATE(d), aHash->m8,
+                                 Sha256::Hash::kSize, MBEDTLS_MD_SHA256);
+#endif
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+
+    OT_ASSERT(mbedtls_mpi_size(&r) <= Ecdsa::P256::kMpiSize);
+
+    ret = mbedtls_mpi_write_binary(&r, aSignature->m8, Ecdsa::P256::kMpiSize);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+
+    ret = mbedtls_mpi_write_binary(&s, aSignature->m8 + Ecdsa::P256::kMpiSize, Ecdsa::P256::kMpiSize);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+
+exit:
+    mbedtls_pk_free(&pk);
+    mbedtls_mpi_free(&s);
+    mbedtls_mpi_free(&r);
+    mbedtls_ecdsa_free(&ecdsa);
+
+    return error;
+}
+
+OT_TOOL_WEAK otError otPlatCryptoEcdsaVerify(const otPlatCryptoEcdsaPublicKey *aPublicKey,
+                                             const otPlatCryptoSha256Hash     *aHash,
+                                             const otPlatCryptoEcdsaSignature *aSignature)
+{
+    Error                 error = kErrorNone;
+    mbedtls_ecdsa_context ecdsa;
+    mbedtls_mpi           r;
+    mbedtls_mpi           s;
+    int                   ret;
+
+    mbedtls_ecdsa_init(&ecdsa);
+    mbedtls_mpi_init(&r);
+    mbedtls_mpi_init(&s);
+
+    ret = mbedtls_ecp_group_load(&ecdsa.MBEDTLS_PRIVATE(grp), MBEDTLS_ECP_DP_SECP256R1);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+
+    ret = mbedtls_mpi_read_binary(&ecdsa.MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), aPublicKey->m8, Ecdsa::P256::kMpiSize);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+    ret = mbedtls_mpi_read_binary(&ecdsa.MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y), aPublicKey->m8 + Ecdsa::P256::kMpiSize,
+                                  Ecdsa::P256::kMpiSize);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+    ret = mbedtls_mpi_lset(&ecdsa.MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Z), 1);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+
+    ret = mbedtls_mpi_read_binary(&r, aSignature->m8, Ecdsa::P256::kMpiSize);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+
+    ret = mbedtls_mpi_read_binary(&s, aSignature->m8 + Ecdsa::P256::kMpiSize, Ecdsa::P256::kMpiSize);
+    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
+
+    ret = mbedtls_ecdsa_verify(&ecdsa.MBEDTLS_PRIVATE(grp), aHash->m8, Sha256::Hash::kSize, &ecdsa.MBEDTLS_PRIVATE(Q),
+                               &r, &s);
+    VerifyOrExit(ret == 0, error = kErrorSecurity);
+
+exit:
+    mbedtls_mpi_free(&s);
+    mbedtls_mpi_free(&r);
+    mbedtls_ecdsa_free(&ecdsa);
+
+    return error;
+}
+
+#endif // #if OPENTHREAD_CONFIG_ECDSA_ENABLE
+
+#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
+
+//---------------------------------------------------------------------------------------------------------------------
+// APIs to be used in "hybrid" mode by every OPENTHREAD_CONFIG_CRYPTO_LIB variant until full PSA support is ready
+
+#if OPENTHREAD_FTD
+
+OT_TOOL_WEAK void otPlatCryptoPbkdf2GenerateKey(const uint8_t *aPassword,
+                                                uint16_t       aPasswordLen,
+                                                const uint8_t *aSalt,
+                                                uint16_t       aSaltLen,
+                                                uint32_t       aIterationCounter,
+                                                uint16_t       aKeyLen,
+                                                uint8_t       *aKey)
+{
+    const size_t kBlockSize = MBEDTLS_CIPHER_BLKSIZE_MAX;
+    uint8_t      prfInput[OT_CRYPTO_PBDKF2_MAX_SALT_SIZE + 4]; // Salt || INT(), for U1 calculation
+    long         prfOne[kBlockSize / sizeof(long)];
+    long         prfTwo[kBlockSize / sizeof(long)];
+    long         keyBlock[kBlockSize / sizeof(long)];
+    uint32_t     blockCounter = 0;
+    uint8_t     *key          = aKey;
+    uint16_t     keyLen       = aKeyLen;
+    uint16_t     useLen       = 0;
+
+    OT_ASSERT(aSaltLen <= sizeof(prfInput));
+    memcpy(prfInput, aSalt, aSaltLen);
+    OT_ASSERT(aIterationCounter % 2 == 0);
+    aIterationCounter /= 2;
+
+#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
+    // limit iterations to avoid OSS-Fuzz timeouts
+    aIterationCounter = 2;
+#endif
+
+    while (keyLen)
+    {
+        ++blockCounter;
+        prfInput[aSaltLen + 0] = static_cast<uint8_t>(blockCounter >> 24);
+        prfInput[aSaltLen + 1] = static_cast<uint8_t>(blockCounter >> 16);
+        prfInput[aSaltLen + 2] = static_cast<uint8_t>(blockCounter >> 8);
+        prfInput[aSaltLen + 3] = static_cast<uint8_t>(blockCounter);
+
+        // Calculate U_1
+        mbedtls_aes_cmac_prf_128(aPassword, aPasswordLen, prfInput, aSaltLen + 4,
+                                 reinterpret_cast<uint8_t *>(keyBlock));
+
+        // Calculate U_2
+        mbedtls_aes_cmac_prf_128(aPassword, aPasswordLen, reinterpret_cast<const uint8_t *>(keyBlock), kBlockSize,
+                                 reinterpret_cast<uint8_t *>(prfOne));
+
+        for (uint32_t j = 0; j < kBlockSize / sizeof(long); ++j)
+        {
+            keyBlock[j] ^= prfOne[j];
+        }
+
+        for (uint32_t i = 1; i < aIterationCounter; ++i)
+        {
+            // Calculate U_{2 * i - 1}
+            mbedtls_aes_cmac_prf_128(aPassword, aPasswordLen, reinterpret_cast<const uint8_t *>(prfOne), kBlockSize,
+                                     reinterpret_cast<uint8_t *>(prfTwo));
+            // Calculate U_{2 * i}
+            mbedtls_aes_cmac_prf_128(aPassword, aPasswordLen, reinterpret_cast<const uint8_t *>(prfTwo), kBlockSize,
+                                     reinterpret_cast<uint8_t *>(prfOne));
+
+            for (uint32_t j = 0; j < kBlockSize / sizeof(long); ++j)
+            {
+                keyBlock[j] ^= prfOne[j] ^ prfTwo[j];
+            }
+        }
+
+        useLen = Min(keyLen, static_cast<uint16_t>(kBlockSize));
+        memcpy(key, keyBlock, useLen);
+        key += useLen;
+        keyLen -= useLen;
+    }
+}
+
+#endif // #if OPENTHREAD_FTD
diff --git a/src/core/crypto/ecdsa.cpp b/src/core/crypto/ecdsa.cpp
deleted file mode 100644
index b91e4e1..0000000
--- a/src/core/crypto/ecdsa.cpp
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- *  Copyright (c) 2018, 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 ECDSA signing.
- */
-
-#include "ecdsa.hpp"
-
-#if OPENTHREAD_CONFIG_ECDSA_ENABLE
-
-#ifndef MBEDTLS_USE_TINYCRYPT
-
-#include <string.h>
-
-#include <mbedtls/ctr_drbg.h>
-#include <mbedtls/ecdsa.h>
-#include <mbedtls/pk.h>
-#include <mbedtls/version.h>
-
-#include "common/code_utils.hpp"
-#include "common/debug.hpp"
-#include "common/random.hpp"
-#include "crypto/mbedtls.hpp"
-
-namespace ot {
-namespace Crypto {
-namespace Ecdsa {
-
-Error P256::KeyPair::Generate(void)
-{
-    mbedtls_pk_context pk;
-    int                ret;
-
-    mbedtls_pk_init(&pk);
-
-    ret = mbedtls_pk_setup(&pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY));
-    VerifyOrExit(ret == 0);
-
-    ret = mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, mbedtls_pk_ec(pk), MbedTls::CryptoSecurePrng, nullptr);
-    VerifyOrExit(ret == 0);
-
-    ret = mbedtls_pk_write_key_der(&pk, mDerBytes, sizeof(mDerBytes));
-    VerifyOrExit(ret > 0);
-
-    mDerLength = static_cast<uint8_t>(ret);
-
-    memmove(mDerBytes, mDerBytes + sizeof(mDerBytes) - mDerLength, mDerLength);
-
-exit:
-    mbedtls_pk_free(&pk);
-
-    return (ret >= 0) ? kErrorNone : MbedTls::MapError(ret);
-}
-
-Error P256::KeyPair::Parse(void *aContext) const
-{
-    Error               error = kErrorNone;
-    mbedtls_pk_context *pk    = reinterpret_cast<mbedtls_pk_context *>(aContext);
-
-    mbedtls_pk_init(pk);
-
-    VerifyOrExit(mbedtls_pk_setup(pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0, error = kErrorFailed);
-#if (MBEDTLS_VERSION_NUMBER >= 0x03000000)
-    VerifyOrExit(mbedtls_pk_parse_key(pk, mDerBytes, mDerLength, nullptr, 0, MbedTls::CryptoSecurePrng, nullptr) == 0,
-                 error = kErrorParse);
-#else
-    VerifyOrExit(mbedtls_pk_parse_key(pk, mDerBytes, mDerLength, nullptr, 0) == 0, error = kErrorParse);
-#endif
-
-exit:
-    return error;
-}
-
-Error P256::KeyPair::GetPublicKey(PublicKey &aPublicKey) const
-{
-    Error                error;
-    mbedtls_pk_context   pk;
-    mbedtls_ecp_keypair *keyPair;
-    int                  ret;
-
-    SuccessOrExit(error = Parse(&pk));
-
-    keyPair = mbedtls_pk_ec(pk);
-
-    ret = mbedtls_mpi_write_binary(&keyPair->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), aPublicKey.mData, kMpiSize);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-
-    ret = mbedtls_mpi_write_binary(&keyPair->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y), aPublicKey.mData + kMpiSize,
-                                   kMpiSize);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-
-exit:
-    mbedtls_pk_free(&pk);
-    return error;
-}
-
-Error P256::KeyPair::Sign(const Sha256::Hash &aHash, Signature &aSignature) const
-{
-    Error                 error;
-    mbedtls_pk_context    pk;
-    mbedtls_ecp_keypair * keypair;
-    mbedtls_ecdsa_context ecdsa;
-    mbedtls_mpi           r;
-    mbedtls_mpi           s;
-    int                   ret;
-
-    mbedtls_ecdsa_init(&ecdsa);
-    mbedtls_mpi_init(&r);
-    mbedtls_mpi_init(&s);
-
-    SuccessOrExit(error = Parse(&pk));
-
-    keypair = mbedtls_pk_ec(pk);
-
-    ret = mbedtls_ecdsa_from_keypair(&ecdsa, keypair);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-
-#if (MBEDTLS_VERSION_NUMBER >= 0x02130000)
-    ret = mbedtls_ecdsa_sign_det_ext(&ecdsa.MBEDTLS_PRIVATE(grp), &r, &s, &ecdsa.MBEDTLS_PRIVATE(d), aHash.GetBytes(),
-                                     Sha256::Hash::kSize, MBEDTLS_MD_SHA256, MbedTls::CryptoSecurePrng, nullptr);
-#else
-    ret = mbedtls_ecdsa_sign_det(&ecdsa.MBEDTLS_PRIVATE(grp), &r, &s, &ecdsa.MBEDTLS_PRIVATE(d), aHash.GetBytes(),
-                                 Sha256::Hash::kSize, MBEDTLS_MD_SHA256);
-#endif
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-
-    OT_ASSERT(mbedtls_mpi_size(&r) <= kMpiSize);
-
-    ret = mbedtls_mpi_write_binary(&r, aSignature.mShared.mMpis.mR, kMpiSize);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-
-    ret = mbedtls_mpi_write_binary(&s, aSignature.mShared.mMpis.mS, kMpiSize);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-
-exit:
-    mbedtls_pk_free(&pk);
-    mbedtls_mpi_free(&s);
-    mbedtls_mpi_free(&r);
-    mbedtls_ecdsa_free(&ecdsa);
-
-    return error;
-}
-
-Error P256::PublicKey::Verify(const Sha256::Hash &aHash, const Signature &aSignature) const
-{
-    Error                 error = kErrorNone;
-    mbedtls_ecdsa_context ecdsa;
-    mbedtls_mpi           r;
-    mbedtls_mpi           s;
-    int                   ret;
-
-    mbedtls_ecdsa_init(&ecdsa);
-    mbedtls_mpi_init(&r);
-    mbedtls_mpi_init(&s);
-
-    ret = mbedtls_ecp_group_load(&ecdsa.MBEDTLS_PRIVATE(grp), MBEDTLS_ECP_DP_SECP256R1);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-
-    ret = mbedtls_mpi_read_binary(&ecdsa.MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), GetBytes(), kMpiSize);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-    ret = mbedtls_mpi_read_binary(&ecdsa.MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y), GetBytes() + kMpiSize, kMpiSize);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-    ret = mbedtls_mpi_lset(&ecdsa.MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Z), 1);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-
-    ret = mbedtls_mpi_read_binary(&r, aSignature.mShared.mMpis.mR, kMpiSize);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-
-    ret = mbedtls_mpi_read_binary(&s, aSignature.mShared.mMpis.mS, kMpiSize);
-    VerifyOrExit(ret == 0, error = MbedTls::MapError(ret));
-
-    ret = mbedtls_ecdsa_verify(&ecdsa.MBEDTLS_PRIVATE(grp), aHash.GetBytes(), Sha256::Hash::kSize,
-                               &ecdsa.MBEDTLS_PRIVATE(Q), &r, &s);
-    VerifyOrExit(ret == 0, error = kErrorSecurity);
-
-exit:
-    mbedtls_mpi_free(&s);
-    mbedtls_mpi_free(&r);
-    mbedtls_ecdsa_free(&ecdsa);
-
-    return error;
-}
-
-Error Sign(uint8_t *      aOutput,
-           uint16_t &     aOutputLength,
-           const uint8_t *aInputHash,
-           uint16_t       aInputHashLength,
-           const uint8_t *aPrivateKey,
-           uint16_t       aPrivateKeyLength)
-{
-    Error                 error = kErrorNone;
-    mbedtls_ecdsa_context ctx;
-    mbedtls_pk_context    pkCtx;
-    mbedtls_ecp_keypair * keypair;
-    mbedtls_mpi           rMpi;
-    mbedtls_mpi           sMpi;
-
-    mbedtls_pk_init(&pkCtx);
-    mbedtls_ecdsa_init(&ctx);
-    mbedtls_mpi_init(&rMpi);
-    mbedtls_mpi_init(&sMpi);
-
-    // Parse a private key in PEM format.
-#if (MBEDTLS_VERSION_NUMBER >= 0x03000000)
-    VerifyOrExit(mbedtls_pk_parse_key(&pkCtx, aPrivateKey, aPrivateKeyLength, nullptr, 0, MbedTls::CryptoSecurePrng,
-                                      nullptr) == 0,
-                 error = kErrorInvalidArgs);
-#else
-    VerifyOrExit(mbedtls_pk_parse_key(&pkCtx, aPrivateKey, aPrivateKeyLength, nullptr, 0) == 0,
-                 error = kErrorInvalidArgs);
-#endif
-    VerifyOrExit(mbedtls_pk_get_type(&pkCtx) == MBEDTLS_PK_ECKEY, error = kErrorInvalidArgs);
-
-    keypair = mbedtls_pk_ec(pkCtx);
-    OT_ASSERT(keypair != nullptr);
-
-    VerifyOrExit(mbedtls_ecdsa_from_keypair(&ctx, keypair) == 0, error = kErrorFailed);
-
-    // Sign using ECDSA.
-    VerifyOrExit(mbedtls_ecdsa_sign(&ctx.MBEDTLS_PRIVATE(grp), &rMpi, &sMpi, &ctx.MBEDTLS_PRIVATE(d), aInputHash,
-                                    aInputHashLength, MbedTls::CryptoSecurePrng, nullptr) == 0,
-                 error = kErrorFailed);
-    VerifyOrExit(mbedtls_mpi_size(&rMpi) + mbedtls_mpi_size(&sMpi) <= aOutputLength, error = kErrorNoBufs);
-
-    // Concatenate the two octet sequences in the order R and then S.
-    VerifyOrExit(mbedtls_mpi_write_binary(&rMpi, aOutput, mbedtls_mpi_size(&rMpi)) == 0, error = kErrorFailed);
-    aOutputLength = static_cast<uint16_t>(mbedtls_mpi_size(&rMpi));
-
-    VerifyOrExit(mbedtls_mpi_write_binary(&sMpi, aOutput + aOutputLength, mbedtls_mpi_size(&sMpi)) == 0,
-                 error = kErrorFailed);
-    aOutputLength += mbedtls_mpi_size(&sMpi);
-
-exit:
-    mbedtls_mpi_free(&rMpi);
-    mbedtls_mpi_free(&sMpi);
-    mbedtls_ecdsa_free(&ctx);
-    mbedtls_pk_free(&pkCtx);
-
-    return error;
-}
-
-} // namespace Ecdsa
-} // namespace Crypto
-} // namespace ot
-
-#endif // MBEDTLS_USE_TINYCRYPT
-#endif // OPENTHREAD_CONFIG_ECDSA_ENABLE
diff --git a/src/core/crypto/ecdsa.hpp b/src/core/crypto/ecdsa.hpp
index 98b439b..41a693d 100644
--- a/src/core/crypto/ecdsa.hpp
+++ b/src/core/crypto/ecdsa.hpp
@@ -41,8 +41,12 @@
 #include <stdint.h>
 #include <stdlib.h>
 
+#include <openthread/crypto.h>
+#include <openthread/platform/crypto.h>
+
 #include "common/error.hpp"
 #include "crypto/sha256.hpp"
+#include "crypto/storage.hpp"
 
 namespace ot {
 namespace Crypto {
@@ -74,6 +78,9 @@
 
     class PublicKey;
     class KeyPair;
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    class KeyPairAsRef;
+#endif
 
     /**
      * This class represents an ECDSA signature.
@@ -83,13 +90,16 @@
      *
      */
     OT_TOOL_PACKED_BEGIN
-    class Signature
+    class Signature : public otPlatCryptoEcdsaSignature
     {
         friend class KeyPair;
         friend class PublicKey;
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+        friend class KeyPairAsRef;
+#endif
 
     public:
-        static constexpr uint8_t kSize = 2 * kMpiSize; ///< Signature size in bytes (two times the curve MPI size).
+        static constexpr uint8_t kSize = OT_CRYPTO_ECDSA_SIGNATURE_SIZE; ///< Signature size in bytes.
 
         /**
          * This method returns the signature as a byte array.
@@ -97,21 +107,7 @@
          * @returns A pointer to the byte array containing the signature.
          *
          */
-        const uint8_t *GetBytes(void) const { return mShared.mKey; }
-
-    private:
-        OT_TOOL_PACKED_BEGIN
-        struct Mpis
-        {
-            uint8_t mR[kMpiSize];
-            uint8_t mS[kMpiSize];
-        } OT_TOOL_PACKED_END;
-
-        union OT_TOOL_PACKED_FIELD
-        {
-            Mpis    mMpis;
-            uint8_t mKey[kSize];
-        } mShared;
+        const uint8_t *GetBytes(void) const { return m8; }
     } OT_TOOL_PACKED_END;
 
     /**
@@ -120,23 +116,20 @@
      * The key pair is stored using Distinguished Encoding Rules (DER) format (per RFC 5915).
      *
      */
-    class KeyPair
+    class KeyPair : public otPlatCryptoEcdsaKeyPair
     {
     public:
         /**
          * Max buffer size (in bytes) for representing the key-pair in DER format.
          *
          */
-        static constexpr uint8_t kMaxDerSize = 125;
+        static constexpr uint8_t kMaxDerSize = OT_CRYPTO_ECDSA_MAX_DER_SIZE;
 
         /**
          * This constructor initializes a `KeyPair` as empty (no key).
          *
          */
-        KeyPair(void)
-            : mDerLength(0)
-        {
-        }
+        KeyPair(void) { mDerLength = 0; }
 
         /**
          * This method generates and populates the `KeyPair` with a new public/private keys.
@@ -147,7 +140,7 @@
          * @retval kErrorFailed       Failed to generate key.
          *
          */
-        Error Generate(void);
+        Error Generate(void) { return otPlatCryptoEcdsaGenerateKey(this); }
 
         /**
          * This method gets the associated public key from the `KeyPair`.
@@ -158,7 +151,7 @@
          * @retval kErrorParse     The key-pair DER format could not be parsed (invalid format).
          *
          */
-        Error GetPublicKey(PublicKey &aPublicKey) const;
+        Error GetPublicKey(PublicKey &aPublicKey) const { return otPlatCryptoEcdsaGetPublicKey(this, &aPublicKey); }
 
         /**
          * This method gets the pointer to start of the buffer containing the key-pair info in DER format.
@@ -212,14 +205,110 @@
          * @retval kErrorNoBufs         Failed to allocate buffer for signature calculation.
          *
          */
-        Error Sign(const Sha256::Hash &aHash, Signature &aSignature) const;
+        Error Sign(const Sha256::Hash &aHash, Signature &aSignature) const
+        {
+            return otPlatCryptoEcdsaSign(this, &aHash, &aSignature);
+        }
+    };
+
+#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:
-        Error Parse(void *aContext) const;
-
-        uint8_t mDerBytes[kMaxDerSize];
-        uint8_t mDerLength;
+        otCryptoKeyRef mKeyRef;
     };
+#endif
 
     /**
      * This class represents a public key.
@@ -228,12 +317,15 @@
      *
      */
     OT_TOOL_PACKED_BEGIN
-    class PublicKey : public Equatable<PublicKey>
+    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 = kMpiSize * 2; ///< Size of the public key in bytes.
+        static constexpr uint8_t kSize = OT_CRYPTO_ECDSA_PUBLIC_KEY_SIZE; ///< Size of the public key in bytes.
 
         /**
          * This method gets the pointer to the buffer containing the public key (as an uncompressed curve point).
@@ -241,7 +333,7 @@
          * @return The pointer to the buffer containing the public key (with `kSize` bytes).
          *
          */
-        const uint8_t *GetBytes(void) const { return mData; }
+        const uint8_t *GetBytes(void) const { return m8; }
 
         /**
          * This method uses the `PublicKey` to verify the ECDSA signature of a hashed message.
@@ -255,43 +347,26 @@
          * @retval kErrorNoBufs        Failed to allocate buffer for signature verification
          *
          */
-        Error Verify(const Sha256::Hash &aHash, const Signature &aSignature) const;
+        Error Verify(const Sha256::Hash &aHash, const Signature &aSignature) const
+        {
+            return otPlatCryptoEcdsaVerify(this, &aHash, &aSignature);
+        }
 
-    private:
-        uint8_t mData[kSize];
     } OT_TOOL_PACKED_END;
 };
 
 /**
- * This function creates an ECDSA signature.
- *
- * @param[out]     aOutput            An output buffer where ECDSA sign should be stored.
- * @param[in,out]  aOutputLength      The length of the @p aOutput buffer.
- * @param[in]      aInputHash         An input hash.
- * @param[in]      aInputHashLength   The length of the @p aInputHash buffer.
- * @param[in]      aPrivateKey        A private key in PEM format.
- * @param[in]      aPrivateKeyLength  The length of the @p aPrivateKey buffer.
- *
- * @retval  kErrorNone         ECDSA sign has been created successfully.
- * @retval  kErrorNoBufs       Output buffer is too small.
- * @retval  kErrorInvalidArgs  Private key is not valid EC Private Key.
- * @retval  kErrorFailed       Error during signing.
- *
- */
-Error Sign(uint8_t *      aOutput,
-           uint16_t &     aOutputLength,
-           const uint8_t *aInputHash,
-           uint16_t       aInputHashLength,
-           const uint8_t *aPrivateKey,
-           uint16_t       aPrivateKeyLength);
-
-/**
  * @}
  *
  */
 
 } // namespace Ecdsa
 } // namespace Crypto
+
+DefineCoreType(otPlatCryptoEcdsaSignature, Crypto::Ecdsa::P256::Signature);
+DefineCoreType(otPlatCryptoEcdsaKeyPair, Crypto::Ecdsa::P256::KeyPair);
+DefineCoreType(otPlatCryptoEcdsaPublicKey, Crypto::Ecdsa::P256::PublicKey);
+
 } // namespace ot
 
 #endif // OPENTHREAD_CONFIG_ECDSA_ENABLE
diff --git a/src/core/crypto/ecdsa_tinycrypt.cpp b/src/core/crypto/ecdsa_tinycrypt.cpp
deleted file mode 100644
index ea75a2d..0000000
--- a/src/core/crypto/ecdsa_tinycrypt.cpp
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- *  Copyright (c) 2022, 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 ECDSA signing using TinyCrypt library.
- */
-
-#include "ecdsa.hpp"
-
-#if OPENTHREAD_CONFIG_ECDSA_ENABLE
-
-#ifdef MBEDTLS_USE_TINYCRYPT
-
-#include <string.h>
-
-#include <mbedtls/pk.h>
-#include <mbedtls/version.h>
-
-#include <tinycrypt/ecc.h>
-#include <tinycrypt/ecc_dh.h>
-#include <tinycrypt/ecc_dsa.h>
-
-#include "common/code_utils.hpp"
-#include "common/debug.hpp"
-#include "common/random.hpp"
-#include "crypto/mbedtls.hpp"
-
-namespace ot {
-namespace Crypto {
-namespace Ecdsa {
-
-Error P256::KeyPair::Generate(void)
-{
-    mbedtls_pk_context    pk;
-    mbedtls_uecc_keypair *keypair;
-    int                   ret;
-
-    mbedtls_pk_init(&pk);
-
-    ret = mbedtls_pk_setup(&pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY));
-    VerifyOrExit(ret == 0);
-
-    keypair = mbedtls_pk_uecc(pk);
-
-    ret = uECC_make_key(keypair->public_key, keypair->private_key);
-    VerifyOrExit(ret == UECC_SUCCESS);
-
-    ret = mbedtls_pk_write_key_der(&pk, mDerBytes, sizeof(mDerBytes));
-    VerifyOrExit(ret > 0);
-
-    mDerLength = static_cast<uint8_t>(ret);
-
-    memmove(mDerBytes, mDerBytes + sizeof(mDerBytes) - mDerLength, mDerLength);
-
-exit:
-    mbedtls_pk_free(&pk);
-
-    return (ret >= 0) ? kErrorNone : MbedTls::MapError(ret);
-}
-
-Error P256::KeyPair::Parse(void *aContext) const
-{
-    Error               error = kErrorNone;
-    mbedtls_pk_context *pk    = reinterpret_cast<mbedtls_pk_context *>(aContext);
-
-    mbedtls_pk_init(pk);
-
-    VerifyOrExit(mbedtls_pk_setup(pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0, error = kErrorFailed);
-#if (MBEDTLS_VERSION_NUMBER >= 0x03000000)
-    VerifyOrExit(mbedtls_pk_parse_key(pk, mDerBytes, mDerLength, nullptr, 0, MbedTls::CryptoSecurePrng, nullptr) == 0,
-                 error = kErrorParse);
-#else
-    VerifyOrExit(mbedtls_pk_parse_key(pk, mDerBytes, mDerLength, nullptr, 0) == 0, error = kErrorParse);
-#endif
-
-exit:
-    return error;
-}
-
-Error P256::KeyPair::GetPublicKey(PublicKey &aPublicKey) const
-{
-    Error                 error;
-    mbedtls_pk_context    pk;
-    mbedtls_uecc_keypair *keyPair;
-    int                   ret;
-
-    SuccessOrExit(error = Parse(&pk));
-
-    keyPair = mbedtls_pk_uecc(pk);
-
-    memcpy(aPublicKey.mData, keyPair->public_key, kMpiSize);
-    memcpy(aPublicKey.mData + kMpiSize, keyPair->public_key + kMpiSize, kMpiSize);
-
-exit:
-    mbedtls_pk_free(&pk);
-
-    return error;
-}
-
-Error P256::KeyPair::Sign(const Sha256::Hash &aHash, Signature &aSignature) const
-{
-    Error                 error;
-    mbedtls_pk_context    pk;
-    mbedtls_uecc_keypair *keypair;
-    int                   ret;
-    uint8_t               sig[2 * kMpiSize];
-
-    SuccessOrExit(error = Parse(&pk));
-
-    keypair = mbedtls_pk_uecc(pk);
-
-    ret = uECC_sign(keypair->private_key, aHash.GetBytes(), Sha256::Hash::kSize, sig);
-    VerifyOrExit(ret == UECC_SUCCESS, error = MbedTls::MapError(ret));
-
-    memcpy(aSignature.mShared.mMpis.mR, sig, kMpiSize);
-    memcpy(aSignature.mShared.mMpis.mS, sig + kMpiSize, kMpiSize);
-
-exit:
-    mbedtls_pk_free(&pk);
-
-    return error;
-}
-
-Error P256::PublicKey::Verify(const Sha256::Hash &aHash, const Signature &aSignature) const
-{
-    Error   error = kErrorNone;
-    int     ret;
-    uint8_t public_key[2 * kMpiSize];
-    uint8_t sig[2 * kMpiSize];
-
-    memcpy(public_key, GetBytes(), 2 * kMpiSize);
-
-    memcpy(sig, aSignature.mShared.mMpis.mR, kMpiSize);
-    memcpy(sig + kMpiSize, aSignature.mShared.mMpis.mS, kMpiSize);
-
-    ret = uECC_verify(public_key, aHash.GetBytes(), Sha256::Hash::kSize, sig);
-    VerifyOrExit(ret == UECC_SUCCESS, error = kErrorSecurity);
-
-exit:
-    return error;
-}
-
-Error Sign(uint8_t *      aOutput,
-           uint16_t &     aOutputLength,
-           const uint8_t *aInputHash,
-           uint16_t       aInputHashLength,
-           const uint8_t *aPrivateKey,
-           uint16_t       aPrivateKeyLength)
-{
-    Error                 error = kErrorNone;
-    mbedtls_pk_context    pkCtx;
-    mbedtls_uecc_keypair *keypair;
-    uint8_t               sig[2 * NUM_ECC_BYTES];
-
-    mbedtls_pk_init(&pkCtx);
-
-    // Parse a private key in PEM format.
-    VerifyOrExit(mbedtls_pk_parse_key(&pkCtx, aPrivateKey, aPrivateKeyLength, nullptr, 0) == 0,
-                 error = kErrorInvalidArgs);
-    VerifyOrExit(mbedtls_pk_get_type(&pkCtx) == MBEDTLS_PK_ECKEY, error = kErrorInvalidArgs);
-
-    keypair = mbedtls_pk_uecc(pkCtx);
-    OT_ASSERT(keypair != nullptr);
-
-    // Sign using ECDSA.
-    VerifyOrExit(uECC_sign(keypair->private_key, aInputHash, aInputHashLength, sig) == UECC_SUCCESS,
-                 error = kErrorFailed);
-    VerifyOrExit(2 * NUM_ECC_BYTES <= aOutputLength, error = kErrorNoBufs);
-
-    // Concatenate the two octet sequences in the order R and then S.
-    memcpy(aOutput, sig, 2 * NUM_ECC_BYTES);
-    aOutputLength = 2 * NUM_ECC_BYTES;
-
-exit:
-    mbedtls_pk_free(&pkCtx);
-
-    return error;
-}
-
-} // namespace Ecdsa
-} // namespace Crypto
-} // namespace ot
-
-#endif // MBEDTLS_USE_TINYCRYPT
-#endif // OPENTHREAD_CONFIG_ECDSA_ENABLE
diff --git a/src/core/crypto/hkdf_sha256.cpp b/src/core/crypto/hkdf_sha256.cpp
index 7c195e6..16ca208 100644
--- a/src/core/crypto/hkdf_sha256.cpp
+++ b/src/core/crypto/hkdf_sha256.cpp
@@ -50,10 +50,7 @@
     SuccessOrAssert(otPlatCryptoHkdfInit(&mContext));
 }
 
-HkdfSha256::~HkdfSha256(void)
-{
-    SuccessOrAssert(otPlatCryptoHkdfDeinit(&mContext));
-}
+HkdfSha256::~HkdfSha256(void) { SuccessOrAssert(otPlatCryptoHkdfDeinit(&mContext)); }
 
 void HkdfSha256::Extract(const uint8_t *aSalt, uint16_t aSaltLength, const Key &aInputKey)
 {
diff --git a/src/core/crypto/hmac_sha256.cpp b/src/core/crypto/hmac_sha256.cpp
index 2cedbac..0f49da0 100644
--- a/src/core/crypto/hmac_sha256.cpp
+++ b/src/core/crypto/hmac_sha256.cpp
@@ -47,15 +47,9 @@
     SuccessOrAssert(otPlatCryptoHmacSha256Init(&mContext));
 }
 
-HmacSha256::~HmacSha256(void)
-{
-    SuccessOrAssert(otPlatCryptoHmacSha256Deinit(&mContext));
-}
+HmacSha256::~HmacSha256(void) { SuccessOrAssert(otPlatCryptoHmacSha256Deinit(&mContext)); }
 
-void HmacSha256::Start(const Key &aKey)
-{
-    SuccessOrAssert(otPlatCryptoHmacSha256Start(&mContext, &aKey));
-}
+void HmacSha256::Start(const Key &aKey) { SuccessOrAssert(otPlatCryptoHmacSha256Start(&mContext, &aKey)); }
 
 void HmacSha256::Update(const void *aBuf, uint16_t aBufLength)
 {
diff --git a/src/core/crypto/pbkdf2_cmac.cpp b/src/core/crypto/pbkdf2_cmac.cpp
deleted file mode 100644
index 988a577..0000000
--- a/src/core/crypto/pbkdf2_cmac.cpp
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 PBKDF2 using AES-CMAC-PRF-128
- */
-
-#include "pbkdf2_cmac.hpp"
-
-#include <mbedtls/cmac.h>
-#include <string.h>
-
-#include "common/debug.hpp"
-
-namespace ot {
-namespace Crypto {
-namespace Pbkdf2 {
-
-#if OPENTHREAD_FTD
-
-void GenerateKey(const uint8_t *aPassword,
-                 uint16_t       aPasswordLen,
-                 const uint8_t *aSalt,
-                 uint16_t       aSaltLen,
-                 uint32_t       aIterationCounter,
-                 uint16_t       aKeyLen,
-                 uint8_t *      aKey)
-{
-    const size_t kBlockSize = MBEDTLS_CIPHER_BLKSIZE_MAX;
-    uint8_t      prfInput[kMaxSaltLength + 4]; // Salt || INT(), for U1 calculation
-    long         prfOne[kBlockSize / sizeof(long)];
-    long         prfTwo[kBlockSize / sizeof(long)];
-    long         keyBlock[kBlockSize / sizeof(long)];
-    uint32_t     blockCounter = 0;
-    uint8_t *    key          = aKey;
-    uint16_t     keyLen       = aKeyLen;
-    uint16_t     useLen       = 0;
-
-    memcpy(prfInput, aSalt, aSaltLen);
-    OT_ASSERT(aIterationCounter % 2 == 0);
-    aIterationCounter /= 2;
-
-#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
-    // limit iterations to avoid OSS-Fuzz timeouts
-    aIterationCounter = 2;
-#endif
-
-    while (keyLen)
-    {
-        ++blockCounter;
-        prfInput[aSaltLen + 0] = static_cast<uint8_t>(blockCounter >> 24);
-        prfInput[aSaltLen + 1] = static_cast<uint8_t>(blockCounter >> 16);
-        prfInput[aSaltLen + 2] = static_cast<uint8_t>(blockCounter >> 8);
-        prfInput[aSaltLen + 3] = static_cast<uint8_t>(blockCounter);
-
-        // Calculate U_1
-        mbedtls_aes_cmac_prf_128(aPassword, aPasswordLen, prfInput, aSaltLen + 4,
-                                 reinterpret_cast<uint8_t *>(keyBlock));
-
-        // Calculate U_2
-        mbedtls_aes_cmac_prf_128(aPassword, aPasswordLen, reinterpret_cast<const uint8_t *>(keyBlock), kBlockSize,
-                                 reinterpret_cast<uint8_t *>(prfOne));
-
-        for (uint32_t j = 0; j < kBlockSize / sizeof(long); ++j)
-        {
-            keyBlock[j] ^= prfOne[j];
-        }
-
-        for (uint32_t i = 1; i < aIterationCounter; ++i)
-        {
-            // Calculate U_{2 * i - 1}
-            mbedtls_aes_cmac_prf_128(aPassword, aPasswordLen, reinterpret_cast<const uint8_t *>(prfOne), kBlockSize,
-                                     reinterpret_cast<uint8_t *>(prfTwo));
-            // Calculate U_{2 * i}
-            mbedtls_aes_cmac_prf_128(aPassword, aPasswordLen, reinterpret_cast<const uint8_t *>(prfTwo), kBlockSize,
-                                     reinterpret_cast<uint8_t *>(prfOne));
-
-            for (uint32_t j = 0; j < kBlockSize / sizeof(long); ++j)
-            {
-                keyBlock[j] ^= prfOne[j] ^ prfTwo[j];
-            }
-        }
-
-        useLen = (keyLen < kBlockSize) ? keyLen : kBlockSize;
-        memcpy(key, keyBlock, useLen);
-        key += useLen;
-        keyLen -= useLen;
-    }
-}
-
-#endif // OPENTHREAD_FTD
-
-} // namespace Pbkdf2
-} // namespace Crypto
-} // namespace ot
diff --git a/src/core/crypto/pbkdf2_cmac.hpp b/src/core/crypto/pbkdf2_cmac.hpp
deleted file mode 100644
index d98e33d..0000000
--- a/src/core/crypto/pbkdf2_cmac.hpp
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- *  Copyright (c) 2016, 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 includes definitions for performing Password-Based Key Derivation Function 2 (PBKDF2) using CMAC.
- */
-
-#ifndef PBKDF2_CMAC_HPP_
-#define PBKDF2_CMAC_HPP_
-
-#include "openthread-core-config.h"
-
-#include <stdint.h>
-
-namespace ot {
-namespace Crypto {
-namespace Pbkdf2 {
-
-/**
- * @addtogroup core-security
- *
- * @{
- *
- */
-
-constexpr uint16_t kMaxSaltLength = 30; ///< Max SALT length: salt prefix (6) + extended panid (8) + network name (16)
-
-/**
- * This function performs PKCS#5 PBKDF2 using CMAC (AES-CMAC-PRF-128).
- *
- * @param[in]     aPassword          Password to use when generating key.
- * @param[in]     aPasswordLen       Length of password.
- * @param[in]     aSalt              Salt to use when generating key.
- * @param[in]     aSaltLen           Length of salt.
- * @param[in]     aIterationCounter  Iteration count.
- * @param[in]     aKeyLen            Length of generated key in bytes.
- * @param[out]    aKey               A pointer to the generated key.
- *
- */
-void GenerateKey(const uint8_t *aPassword,
-                 uint16_t       aPasswordLen,
-                 const uint8_t *aSalt,
-                 uint16_t       aSaltLen,
-                 uint32_t       aIterationCounter,
-                 uint16_t       aKeyLen,
-                 uint8_t *      aKey);
-
-/**
- * @}
- *
- */
-
-} // namespace Pbkdf2
-} // namespace Crypto
-} // namespace ot
-
-#endif // PBKDF2_CMAC_HPP_
diff --git a/src/core/crypto/sha256.cpp b/src/core/crypto/sha256.cpp
index b649994..c4c5225 100644
--- a/src/core/crypto/sha256.cpp
+++ b/src/core/crypto/sha256.cpp
@@ -47,15 +47,9 @@
     SuccessOrAssert(otPlatCryptoSha256Init(&mContext));
 }
 
-Sha256::~Sha256(void)
-{
-    SuccessOrAssert(otPlatCryptoSha256Deinit(&mContext));
-}
+Sha256::~Sha256(void) { SuccessOrAssert(otPlatCryptoSha256Deinit(&mContext)); }
 
-void Sha256::Start(void)
-{
-    SuccessOrAssert(otPlatCryptoSha256Start(&mContext));
-}
+void Sha256::Start(void) { SuccessOrAssert(otPlatCryptoSha256Start(&mContext)); }
 
 void Sha256::Update(const void *aBuf, uint16_t aBufLength)
 {
@@ -75,10 +69,7 @@
     }
 }
 
-void Sha256::Finish(Hash &aHash)
-{
-    SuccessOrAssert(otPlatCryptoSha256Finish(&mContext, aHash.m8, Hash::kSize));
-}
+void Sha256::Finish(Hash &aHash) { SuccessOrAssert(otPlatCryptoSha256Finish(&mContext, aHash.m8, Hash::kSize)); }
 
 } // namespace Crypto
 } // namespace ot
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 725ab27..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.
@@ -112,10 +116,7 @@
  * @retval FALSE  If @p aKeyRef is not valid.
  *
  */
-inline bool IsKeyRefValid(KeyRef aKeyRef)
-{
-    return (aKeyRef < kInvalidKeyRef);
-}
+inline bool IsKeyRefValid(KeyRef aKeyRef) { return (aKeyRef < kInvalidKeyRef); }
 
 /**
  * Import a key into PSA ITS.
@@ -133,7 +134,7 @@
  * @retval kErrorInvalidArgs   @p aKey was set to `nullptr`.
  *
  */
-inline Error ImportKey(KeyRef &       aKeyRef,
+inline Error ImportKey(KeyRef        &aKeyRef,
                        KeyType        aKeyType,
                        KeyAlgorithm   aKeyAlgorithm,
                        int            aKeyUsage,
@@ -187,10 +188,13 @@
  * @retval false                Key Ref passed is invalid and has no key associated in PSA.
  *
  */
-inline bool HasKey(KeyRef aKeyRef)
-{
-    return otPlatCryptoHasKey(aKeyRef);
-}
+inline bool HasKey(KeyRef aKeyRef) { return otPlatCryptoHasKey(aKeyRef); }
+
+/**
+ * Delete all the persistent keys stored in PSA ITS.
+ *
+ */
+void DestroyPersistentKeys(void);
 
 } // namespace Storage
 
@@ -210,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 090aa03..2ddb658 100644
--- a/src/core/diags/README.md
+++ b/src/core/diags/README.md
@@ -9,11 +9,16 @@
 - [diag](#diag)
 - [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)
 - [diag repeat](#diag-repeat-delay-length)
 - [diag radio](#diag-radio-sleep)
+- [diag rawpowersetting](#diag-rawpowersetting)
 - [diag stats](#diag-stats)
+- [diag gpio](#diag-gpio-get-gpio)
 - [diag stop](#diag-stop)
 
 ### diag
@@ -54,6 +59,42 @@
 status 0x00
 ```
 
+### diag cw start
+
+Start transmitting continuous carrier wave.
+
+```bash
+> diag cw start
+Done
+```
+
+### diag cw stop
+
+Stop transmitting continuous carrier wave.
+
+```bash
+> diag cw stop
+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.
@@ -73,6 +114,35 @@
 status 0x00
 ```
 
+### diag powersettings
+
+Show the currently used power settings table.
+
+- Note: The unit of `TargetPower` and `ActualPower` is 0.01dBm.
+
+```bash
+> diag powersettings
+| StartCh | EndCh | TargetPower | ActualPower | RawPowerSetting |
++---------+-------+-------------+-------------+-----------------+
+|      11 |    14 |        1700 |        1000 |          223344 |
+|      15 |    24 |        2000 |        1900 |          112233 |
+|      25 |    25 |        1600 |        1000 |          223344 |
+|      26 |    26 |        1600 |        1500 |          334455 |
+Done
+```
+
+### diag powersettings \<channel\>
+
+Show the currently used power settings for the given channel.
+
+```bash
+> diag powersettings 11
+TargetPower(0.01dBm): 1700
+ActualPower(0.01dBm): 1000
+RawPowerSetting: 223344
+Done
+```
+
 ### diag send \<packets\> \<length\>
 
 Transmit a fixed number of packets with fixed length.
@@ -136,6 +206,43 @@
 sleep
 ```
 
+### diag rawpowersetting
+
+Show the raw power setting for diagnostics module.
+
+```bash
+> diag rawpowersetting
+112233
+Done
+```
+
+### diag rawpowersetting \<settings\>
+
+Set the raw power setting for diagnostics module.
+
+```bash
+> diag rawpowersetting 112233
+Done
+```
+
+### diag rawpowersetting enable
+
+Enable the platform layer to use the value set by the command `diag rawpowersetting \<settings\>`.
+
+```bash
+> diag rawpowersetting enable
+Done
+```
+
+### diag rawpowersetting disable
+
+Disable the platform layer to use the value set by the command `diag rawpowersetting \<settings\>`.
+
+```bash
+> diag rawpowersetting disable
+Done
+```
+
 ### diag stats
 
 Print statistics during diagnostics mode.
@@ -157,6 +264,55 @@
 stats cleared
 ```
 
+### diag gpio get \<gpio\>
+
+Get the gpio value.
+
+```bash
+> diag gpio get 0
+1
+Done
+```
+
+### diag gpio set \<gpio\> \<value\>
+
+Set the gpio value.
+
+The parameter `value` has to be `0` or `1`.
+
+```bash
+> diag gpio set 0 1
+Done
+```
+
+### diag gpio mode \<gpio\>
+
+Get the gpio mode.
+
+```bash
+> diag gpio mode 1
+in
+Done
+```
+
+### diag gpio mode \<gpio\> in
+
+Sets the given gpio to the input mode without pull resistor.
+
+```bash
+> diag gpio mode 1 in
+Done
+```
+
+### diag gpio mode \<gpio\> out
+
+Sets the given gpio to the output mode.
+
+```bash
+> diag gpio mode 1 out
+Done
+```
+
 ### diag stop
 
 Stop diagnostics mode and print statistics.
diff --git a/src/core/diags/factory_diags.cpp b/src/core/diags/factory_diags.cpp
index 6410421..5eed151 100644
--- a/src/core/diags/factory_diags.cpp
+++ b/src/core/diags/factory_diags.cpp
@@ -51,8 +51,8 @@
 OT_TOOL_WEAK
 otError otPlatDiagProcess(otInstance *aInstance,
                           uint8_t     aArgsLength,
-                          char *      aArgs[],
-                          char *      aOutput,
+                          char       *aArgs[],
+                          char       *aOutput,
                           size_t      aOutputMaxLen)
 {
     OT_UNUSED_VARIABLE(aArgsLength);
@@ -70,8 +70,16 @@
 #if OPENTHREAD_RADIO && !OPENTHREAD_RADIO_CLI
 
 const struct Diags::Command Diags::sCommands[] = {
-    {"channel", &Diags::ProcessChannel}, {"echo", &Diags::ProcessEcho}, {"power", &Diags::ProcessPower},
-    {"start", &Diags::ProcessStart},     {"stop", &Diags::ProcessStop},
+    {"channel", &Diags::ProcessChannel},
+    {"cw", &Diags::ProcessContinuousWave},
+    {"echo", &Diags::ProcessEcho},
+    {"gpio", &Diags::ProcessGpio},
+    {"power", &Diags::ProcessPower},
+    {"powersettings", &Diags::ProcessPowerSettings},
+    {"rawpowersetting", &Diags::ProcessRawPowerSetting},
+    {"start", &Diags::ProcessStart},
+    {"stop", &Diags::ProcessStop},
+    {"stream", &Diags::ProcessStream},
 };
 
 Diags::Diags(Instance &aInstance)
@@ -129,8 +137,7 @@
         uint32_t      number;
 
         SuccessOrExit(error = ParseLong(aArgs[1], value));
-        number = static_cast<uint32_t>(value);
-        number = (number < outputMaxLen) ? number : outputMaxLen;
+        number = Min(static_cast<uint32_t>(value), outputMaxLen);
 
         for (i = 0; i < number; i++)
         {
@@ -173,17 +180,24 @@
     return kErrorNone;
 }
 
-extern "C" void otPlatDiagAlarmFired(otInstance *aInstance)
-{
-    otPlatDiagAlarmCallback(aInstance);
-}
+extern "C" void otPlatDiagAlarmFired(otInstance *aInstance) { otPlatDiagAlarmCallback(aInstance); }
 
 #else // OPENTHREAD_RADIO && !OPENTHREAD_RADIO_CLI
 // For OPENTHREAD_FTD, OPENTHREAD_MTD, OPENTHREAD_RADIO_CLI
 const struct Diags::Command Diags::sCommands[] = {
-    {"channel", &Diags::ProcessChannel}, {"power", &Diags::ProcessPower}, {"radio", &Diags::ProcessRadio},
-    {"repeat", &Diags::ProcessRepeat},   {"send", &Diags::ProcessSend},   {"start", &Diags::ProcessStart},
-    {"stats", &Diags::ProcessStats},     {"stop", &Diags::ProcessStop},
+    {"channel", &Diags::ProcessChannel},
+    {"cw", &Diags::ProcessContinuousWave},
+    {"gpio", &Diags::ProcessGpio},
+    {"power", &Diags::ProcessPower},
+    {"powersettings", &Diags::ProcessPowerSettings},
+    {"rawpowersetting", &Diags::ProcessRawPowerSetting},
+    {"radio", &Diags::ProcessRadio},
+    {"repeat", &Diags::ProcessRepeat},
+    {"send", &Diags::ProcessSend},
+    {"start", &Diags::ProcessStart},
+    {"stats", &Diags::ProcessStats},
+    {"stop", &Diags::ProcessStop},
+    {"stream", &Diags::ProcessStream},
 };
 
 Diags::Diags(Instance &aInstance)
@@ -475,10 +489,7 @@
     return error;
 }
 
-extern "C" void otPlatDiagAlarmFired(otInstance *aInstance)
-{
-    AsCoreType(aInstance).Get<Diags>().AlarmFired();
-}
+extern "C" void otPlatDiagAlarmFired(otInstance *aInstance) { AsCoreType(aInstance).Get<Diags>().AlarmFired(); }
 
 void Diags::AlarmFired(void)
 {
@@ -543,6 +554,212 @@
 
 #endif // OPENTHREAD_RADIO
 
+Error Diags::ProcessContinuousWave(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)
+    {
+        SuccessOrExit(error = otPlatDiagRadioTransmitCarrier(&GetInstance(), true));
+    }
+    else if (strcmp(aArgs[0], "stop") == 0)
+    {
+        SuccessOrExit(error = otPlatDiagRadioTransmitCarrier(&GetInstance(), false));
+    }
+
+exit:
+    AppendErrorResult(error, aOutput, aOutputMaxLen);
+    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;
+    return otPlatDiagRadioGetPowerSettings(&GetInstance(), aChannel, &aPowerSettings.mTargetPower,
+                                           &aPowerSettings.mActualPower, aPowerSettings.mRawPowerSetting.mData,
+                                           &aPowerSettings.mRawPowerSetting.mLength);
+}
+
+Error Diags::ProcessPowerSettings(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen)
+{
+    Error         error = kErrorInvalidArgs;
+    uint8_t       channel;
+    PowerSettings powerSettings;
+
+    VerifyOrExit(otPlatDiagModeGet(), error = kErrorInvalidState);
+
+    if (aArgsLength == 0)
+    {
+        bool          isPrePowerSettingsValid = false;
+        uint8_t       preChannel              = 0;
+        PowerSettings prePowerSettings;
+        int           n;
+
+        n = snprintf(aOutput, aOutputMaxLen,
+                     "| StartCh | EndCh | TargetPower | ActualPower | RawPowerSetting |\r\n"
+                     "+---------+-------+-------------+-------------+-----------------+\r\n");
+        VerifyOrExit((n > 0) && (n < static_cast<int>(aOutputMaxLen)), error = kErrorNoBufs);
+        aOutput += n;
+        aOutputMaxLen -= static_cast<size_t>(n);
+
+        for (channel = Radio::kChannelMin; channel <= Radio::kChannelMax + 1; channel++)
+        {
+            error = (channel == Radio::kChannelMax + 1) ? kErrorNotFound : GetPowerSettings(channel, powerSettings);
+
+            if (isPrePowerSettingsValid && ((powerSettings != prePowerSettings) || (error != kErrorNone)))
+            {
+                n = snprintf(aOutput, aOutputMaxLen, "| %7u | %5u | %11d | %11d | %15s |\r\n", preChannel, channel - 1,
+                             prePowerSettings.mTargetPower, prePowerSettings.mActualPower,
+                             prePowerSettings.mRawPowerSetting.ToString().AsCString());
+                VerifyOrExit((n > 0) && (n < static_cast<int>(aOutputMaxLen)), error = kErrorNoBufs);
+                aOutput += n;
+                aOutputMaxLen -= static_cast<size_t>(n);
+                isPrePowerSettingsValid = false;
+            }
+
+            if ((error == kErrorNone) && (!isPrePowerSettingsValid))
+            {
+                preChannel              = channel;
+                prePowerSettings        = powerSettings;
+                isPrePowerSettingsValid = true;
+            }
+        }
+
+        error = kErrorNone;
+    }
+    else if (aArgsLength == 1)
+    {
+        SuccessOrExit(error = Utils::CmdLineParser::ParseAsUint8(aArgs[0], channel));
+        VerifyOrExit(channel >= Radio::kChannelMin && channel <= Radio::kChannelMax, error = kErrorInvalidArgs);
+
+        SuccessOrExit(error = GetPowerSettings(channel, powerSettings));
+        snprintf(aOutput, aOutputMaxLen,
+                 "TargetPower(0.01dBm): %d\r\nActualPower(0.01dBm): %d\r\nRawPowerSetting: %s\r\n",
+                 powerSettings.mTargetPower, powerSettings.mActualPower,
+                 powerSettings.mRawPowerSetting.ToString().AsCString());
+    }
+
+exit:
+    AppendErrorResult(error, aOutput, aOutputMaxLen);
+    return error;
+}
+
+Error Diags::GetRawPowerSetting(RawPowerSetting &aRawPowerSetting)
+{
+    aRawPowerSetting.mLength = RawPowerSetting::kMaxDataSize;
+    return otPlatDiagRadioGetRawPowerSetting(&GetInstance(), aRawPowerSetting.mData, &aRawPowerSetting.mLength);
+}
+
+Error Diags::ProcessRawPowerSetting(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen)
+{
+    Error           error = kErrorInvalidArgs;
+    RawPowerSetting setting;
+
+    VerifyOrExit(otPlatDiagModeGet(), error = kErrorInvalidState);
+
+    if (aArgsLength == 0)
+    {
+        SuccessOrExit(error = GetRawPowerSetting(setting));
+        snprintf(aOutput, aOutputMaxLen, "%s\r\n", setting.ToString().AsCString());
+    }
+    else if (strcmp(aArgs[0], "enable") == 0)
+    {
+        SuccessOrExit(error = otPlatDiagRadioRawPowerSettingEnable(&GetInstance(), true));
+    }
+    else if (strcmp(aArgs[0], "disable") == 0)
+    {
+        SuccessOrExit(error = otPlatDiagRadioRawPowerSettingEnable(&GetInstance(), false));
+    }
+    else
+    {
+        setting.mLength = RawPowerSetting::kMaxDataSize;
+        SuccessOrExit(error = Utils::CmdLineParser::ParseAsHexString(aArgs[0], setting.mLength, setting.mData));
+        SuccessOrExit(error = otPlatDiagRadioSetRawPowerSetting(&GetInstance(), setting.mData, setting.mLength));
+    }
+
+exit:
+    AppendErrorResult(error, aOutput, aOutputMaxLen);
+    return error;
+}
+
+Error Diags::ProcessGpio(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen)
+{
+    Error      error = kErrorInvalidArgs;
+    long       value;
+    uint32_t   gpio;
+    bool       level;
+    otGpioMode mode;
+
+    if ((aArgsLength == 2) && (strcmp(aArgs[0], "get") == 0))
+    {
+        SuccessOrExit(error = ParseLong(aArgs[1], value));
+        gpio = static_cast<uint32_t>(value);
+        SuccessOrExit(error = otPlatDiagGpioGet(gpio, &level));
+        snprintf(aOutput, aOutputMaxLen, "%d\r\n", level);
+    }
+    else if ((aArgsLength == 3) && (strcmp(aArgs[0], "set") == 0))
+    {
+        SuccessOrExit(error = ParseLong(aArgs[1], value));
+        gpio = static_cast<uint32_t>(value);
+        SuccessOrExit(error = ParseBool(aArgs[2], level));
+        SuccessOrExit(error = otPlatDiagGpioSet(gpio, level));
+    }
+    else if ((aArgsLength >= 2) && (strcmp(aArgs[0], "mode") == 0))
+    {
+        SuccessOrExit(error = ParseLong(aArgs[1], value));
+        gpio = static_cast<uint32_t>(value);
+
+        if (aArgsLength == 2)
+        {
+            SuccessOrExit(error = otPlatDiagGpioGetMode(gpio, &mode));
+            if (mode == OT_GPIO_MODE_INPUT)
+            {
+                snprintf(aOutput, aOutputMaxLen, "in\r\n");
+            }
+            else if (mode == OT_GPIO_MODE_OUTPUT)
+            {
+                snprintf(aOutput, aOutputMaxLen, "out\r\n");
+            }
+        }
+        else if ((aArgsLength == 3) && (strcmp(aArgs[2], "in") == 0))
+        {
+            SuccessOrExit(error = otPlatDiagGpioSetMode(gpio, OT_GPIO_MODE_INPUT));
+        }
+        else if ((aArgsLength == 3) && (strcmp(aArgs[2], "out") == 0))
+        {
+            SuccessOrExit(error = otPlatDiagGpioSetMode(gpio, OT_GPIO_MODE_OUTPUT));
+        }
+    }
+
+exit:
+    AppendErrorResult(error, aOutput, aOutputMaxLen);
+    return error;
+}
+
 void Diags::AppendErrorResult(Error aError, char *aOutput, size_t aOutputMaxLen)
 {
     if (aError != kErrorNone)
@@ -558,6 +775,19 @@
     return (*endptr == '\0') ? kErrorNone : kErrorParse;
 }
 
+Error Diags::ParseBool(char *aString, bool &aBool)
+{
+    Error error;
+    long  value;
+
+    SuccessOrExit(error = ParseLong(aString, value));
+    VerifyOrExit((value == 0) || (value == 1), error = kErrorParse);
+    aBool = static_cast<bool>(value);
+
+exit:
+    return error;
+}
+
 Error Diags::ParseCmd(char *aString, uint8_t &aArgsLength, char *aArgs[])
 {
     Error                     error;
@@ -571,13 +801,13 @@
     return error;
 }
 
-void Diags::ProcessLine(const char *aString, char *aOutput, size_t aOutputMaxLen)
+Error Diags::ProcessLine(const char *aString, char *aOutput, size_t aOutputMaxLen)
 {
     constexpr uint16_t kMaxCommandBuffer = OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE;
 
     Error   error = kErrorNone;
     char    buffer[kMaxCommandBuffer];
-    char *  args[kMaxArgs];
+    char   *args[kMaxArgs];
     uint8_t argCount = 0;
 
     VerifyOrExit(StringLength(aString, kMaxCommandBuffer) < kMaxCommandBuffer, error = kErrorNoBufs);
@@ -591,7 +821,7 @@
     {
     case kErrorNone:
         aOutput[0] = '\0'; // In case there is no output.
-        IgnoreError(ProcessCmd(argCount, &args[0], aOutput, aOutputMaxLen));
+        error      = ProcessCmd(argCount, &args[0], aOutput, aOutputMaxLen);
         break;
 
     case kErrorNoBufs:
@@ -606,6 +836,8 @@
         snprintf(aOutput, aOutputMaxLen, "failed to parse command string\r\n");
         break;
     }
+
+    return error;
 }
 
 Error Diags::ProcessCmd(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen)
@@ -655,12 +887,103 @@
     return error;
 }
 
-bool Diags::IsEnabled(void)
-{
-    return otPlatDiagModeGet();
-}
+bool Diags::IsEnabled(void) { return otPlatDiagModeGet(); }
 
 } // namespace FactoryDiags
 } // namespace ot
 
+OT_TOOL_WEAK otError otPlatDiagGpioSet(uint32_t aGpio, bool aValue)
+{
+    OT_UNUSED_VARIABLE(aGpio);
+    OT_UNUSED_VARIABLE(aValue);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+OT_TOOL_WEAK otError otPlatDiagGpioGet(uint32_t aGpio, bool *aValue)
+{
+    OT_UNUSED_VARIABLE(aGpio);
+    OT_UNUSED_VARIABLE(aValue);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+OT_TOOL_WEAK otError otPlatDiagGpioSetMode(uint32_t aGpio, otGpioMode aMode)
+{
+    OT_UNUSED_VARIABLE(aGpio);
+    OT_UNUSED_VARIABLE(aMode);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+OT_TOOL_WEAK otError otPlatDiagGpioGetMode(uint32_t aGpio, otGpioMode *aMode)
+{
+    OT_UNUSED_VARIABLE(aGpio);
+    OT_UNUSED_VARIABLE(aMode);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+OT_TOOL_WEAK otError otPlatDiagRadioSetRawPowerSetting(otInstance    *aInstance,
+                                                       const uint8_t *aRawPowerSetting,
+                                                       uint16_t       aRawPowerSettingLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aRawPowerSetting);
+    OT_UNUSED_VARIABLE(aRawPowerSettingLength);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+OT_TOOL_WEAK otError otPlatDiagRadioGetRawPowerSetting(otInstance *aInstance,
+                                                       uint8_t    *aRawPowerSetting,
+                                                       uint16_t   *aRawPowerSettingLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aRawPowerSetting);
+    OT_UNUSED_VARIABLE(aRawPowerSettingLength);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+OT_TOOL_WEAK otError otPlatDiagRadioRawPowerSettingEnable(otInstance *aInstance, bool aEnable)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aEnable);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
+OT_TOOL_WEAK otError otPlatDiagRadioTransmitCarrier(otInstance *aInstance, bool aEnable)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aEnable);
+
+    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,
+                                                     int16_t    *aActualPower,
+                                                     uint8_t    *aRawPowerSetting,
+                                                     uint16_t   *aRawPowerSettingLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aChannel);
+    OT_UNUSED_VARIABLE(aTargetPower);
+    OT_UNUSED_VARIABLE(aActualPower);
+    OT_UNUSED_VARIABLE(aRawPowerSetting);
+    OT_UNUSED_VARIABLE(aRawPowerSettingLength);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
 #endif // OPENTHREAD_CONFIG_DIAG_ENABLE
diff --git a/src/core/diags/factory_diags.hpp b/src/core/diags/factory_diags.hpp
index 26ccd9e..d7ac648 100644
--- a/src/core/diags/factory_diags.hpp
+++ b/src/core/diags/factory_diags.hpp
@@ -45,6 +45,7 @@
 #include "common/error.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
+#include "common/string.hpp"
 
 namespace ot {
 namespace FactoryDiags {
@@ -68,7 +69,7 @@
      * @param[in]   aOutputMaxLen  The output buffer size.
      *
      */
-    void ProcessLine(const char *aString, char *aOutput, size_t aOutputMaxLen);
+    Error ProcessLine(const char *aString, char *aOutput, size_t aOutputMaxLen);
 
     /**
      * This method processes a factory diagnostics command line.
@@ -142,23 +143,69 @@
         uint8_t  mLastLqi;
     };
 
+    struct RawPowerSetting
+    {
+        static constexpr uint16_t       kMaxDataSize    = OPENTHREAD_CONFIG_POWER_CALIBRATION_RAW_POWER_SETTING_SIZE;
+        static constexpr uint16_t       kInfoStringSize = kMaxDataSize * 2 + 1;
+        typedef String<kInfoStringSize> InfoString;
+
+        InfoString ToString(void) const
+        {
+            InfoString string;
+
+            string.AppendHexBytes(mData, mLength);
+
+            return string;
+        }
+
+        bool operator!=(const RawPowerSetting &aOther) const
+        {
+            return (mLength != aOther.mLength) || (memcmp(mData, aOther.mData, mLength) != 0);
+        }
+
+        uint8_t  mData[kMaxDataSize];
+        uint16_t mLength;
+    };
+
+    struct PowerSettings
+    {
+        bool operator!=(const PowerSettings &aOther) const
+        {
+            return (mTargetPower != aOther.mTargetPower) || (mActualPower != aOther.mActualPower) ||
+                   (mRawPowerSetting != aOther.mRawPowerSetting);
+        }
+
+        int16_t         mTargetPower;
+        int16_t         mActualPower;
+        RawPowerSetting mRawPowerSetting;
+    };
+
     Error ParseCmd(char *aString, uint8_t &aArgsLength, char *aArgs[]);
     Error ProcessChannel(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
+    Error ProcessContinuousWave(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
+    Error ProcessGpio(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
     Error ProcessPower(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
     Error ProcessRadio(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
     Error ProcessRepeat(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
+    Error ProcessPowerSettings(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
+    Error ProcessRawPowerSetting(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
     Error ProcessSend(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
     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
 
+    Error GetRawPowerSetting(RawPowerSetting &aRawPowerSetting);
+    Error GetPowerSettings(uint8_t aChannel, PowerSettings &aPowerSettings);
+
     void TransmitPacket(void);
 
     static void  AppendErrorResult(Error aError, char *aOutput, size_t aOutputMaxLen);
     static Error ParseLong(char *aString, long &aLong);
+    static Error ParseBool(char *aString, bool &aBool);
 
     static const struct Command sCommands[];
 
diff --git a/src/core/ftd.cmake b/src/core/ftd.cmake
index 0100a1f..dd6dae2 100644
--- a/src/core/ftd.cmake
+++ b/src/core/ftd.cmake
@@ -43,9 +43,8 @@
 target_link_libraries(openthread-ftd
     PRIVATE
         ${OT_MBEDTLS}
+        ot-config-ftd
         ot-config
 )
 
-if(NOT OT_EXCLUDE_TCPLP_LIB)
-    target_link_libraries(openthread-ftd PRIVATE tcplp)
-endif()
+target_link_libraries(openthread-ftd PRIVATE tcplp-ftd)
diff --git a/src/core/mac/data_poll_handler.cpp b/src/core/mac/data_poll_handler.cpp
index c1d78e3..b503318 100644
--- a/src/core/mac/data_poll_handler.cpp
+++ b/src/core/mac/data_poll_handler.cpp
@@ -51,7 +51,7 @@
 
 inline Error DataPollHandler::Callbacks::PrepareFrameForChild(Mac::TxFrame &aFrame,
                                                               FrameContext &aContext,
-                                                              Child &       aChild)
+                                                              Child        &aChild)
 {
     return Get<IndirectSender>().PrepareFrameForChild(aFrame, aContext, aChild);
 }
@@ -59,7 +59,7 @@
 inline void DataPollHandler::Callbacks::HandleSentFrameToChild(const Mac::TxFrame &aFrame,
                                                                const FrameContext &aContext,
                                                                Error               aError,
-                                                               Child &             aChild)
+                                                               Child              &aChild)
 {
     Get<IndirectSender>().HandleSentFrameToChild(aFrame, aContext, aError, aChild);
 }
@@ -128,7 +128,7 @@
 void DataPollHandler::HandleDataPoll(Mac::RxFrame &aFrame)
 {
     Mac::Address macSource;
-    Child *      child;
+    Child       *child;
     uint16_t     indirectMsgCount;
 
     VerifyOrExit(aFrame.GetSecurityEnabled());
@@ -188,7 +188,11 @@
     VerifyOrExit(mCallbacks.PrepareFrameForChild(*frame, mFrameContext, *mIndirectTxChild) == kErrorNone,
                  frame = nullptr);
 
+#if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
+    if ((mIndirectTxChild->GetIndirectTxAttempts() > 0) || (mIndirectTxChild->GetCslTxAttempts() > 0))
+#else
     if (mIndirectTxChild->GetIndirectTxAttempts() > 0)
+#endif
     {
         // For a re-transmission of an indirect frame to a sleepy
         // child, we ensure to use the same frame counter, key id, and
@@ -240,6 +244,9 @@
     {
     case kErrorNone:
         aChild.ResetIndirectTxAttempts();
+#if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
+        aChild.ResetCslTxAttempts();
+#endif
         aChild.SetFrameReplacePending(false);
         break;
 
@@ -291,7 +298,6 @@
 
     default:
         OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
     }
 
     mCallbacks.HandleSentFrameToChild(aFrame, mFrameContext, aError, aChild);
diff --git a/src/core/mac/data_poll_handler.hpp b/src/core/mac/data_poll_handler.hpp
index 37532c4..11cb531 100644
--- a/src/core/mac/data_poll_handler.hpp
+++ b/src/core/mac/data_poll_handler.hpp
@@ -191,7 +191,7 @@
         void HandleSentFrameToChild(const Mac::TxFrame &aFrame,
                                     const FrameContext &aContext,
                                     Error               aError,
-                                    Child &             aChild);
+                                    Child              &aChild);
 
         /**
          * This callback method notifies that a requested frame change from `RequestFrameChange()` is processed.
@@ -282,7 +282,7 @@
     // indicates no active indirect tx). `mFrameContext` tracks the
     // context for the prepared frame for the current indirect tx.
 
-    Child *                 mIndirectTxChild;
+    Child                  *mIndirectTxChild;
     Callbacks::FrameContext mFrameContext;
     Callbacks               mCallbacks;
 };
diff --git a/src/core/mac/data_poll_sender.cpp b/src/core/mac/data_poll_sender.cpp
index 2c5b089..acc968c 100644
--- a/src/core/mac/data_poll_sender.cpp
+++ b/src/core/mac/data_poll_sender.cpp
@@ -38,6 +38,7 @@
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
 #include "common/message.hpp"
+#include "common/num_utils.hpp"
 #include "net/ip6.hpp"
 #include "net/netif.hpp"
 #include "thread/mesh_forwarder.hpp"
@@ -54,7 +55,7 @@
     , mPollPeriod(0)
     , mExternalPollPeriod(0)
     , mFastPollsUsers(0)
-    , mTimer(aInstance, DataPollSender::HandlePollTimer)
+    , mTimer(aInstance)
     , mEnabled(false)
     , mAttachMode(false)
     , mRetxMode(false)
@@ -197,7 +198,7 @@
 
     if (mExternalPollPeriod != 0)
     {
-        period = OT_MIN(period, mExternalPollPeriod);
+        period = Min(period, mExternalPollPeriod);
     }
 
     return period;
@@ -506,22 +507,29 @@
 
     if (mAttachMode)
     {
-        period = OT_MIN(period, kAttachDataPollPeriod);
+        period = Min(period, kAttachDataPollPeriod);
     }
 
     if (mRetxMode)
     {
-        period = OT_MIN(period, kRetxPollPeriod);
+        period = Min(period, kRetxPollPeriod);
+
+#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+        if (Get<Mac::Mac>().GetCslPeriodMs() > 0)
+        {
+            period = Min(period, Get<Mac::Mac>().GetCslPeriodMs());
+        }
+#endif
     }
 
     if (mRemainingFastPolls != 0)
     {
-        period = OT_MIN(period, kFastPollPeriod);
+        period = Min(period, kFastPollPeriod);
     }
 
     if (mExternalPollPeriod != 0)
     {
-        period = OT_MIN(period, mExternalPollPeriod);
+        period = Min(period, mExternalPollPeriod);
     }
 
     if (period == 0)
@@ -532,20 +540,17 @@
     return period;
 }
 
-void DataPollSender::HandlePollTimer(Timer &aTimer)
-{
-    IgnoreError(aTimer.Get<DataPollSender>().SendDataPoll());
-}
-
 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())
     {
-        period    = OT_MIN(period, Time::SecToMsec(Get<Mle::MleRouter>().GetCslTimeout()));
+        period    = Min(period, Time::SecToMsec(Get<Mle::MleRouter>().GetCslTimeout()));
         pollAhead = static_cast<uint32_t>(kRetxPollPeriod);
     }
 #endif
@@ -560,67 +565,42 @@
 
 Mac::TxFrame *DataPollSender::PrepareDataRequest(Mac::TxFrames &aTxFrames)
 {
-    Mac::TxFrame *frame = nullptr;
-    Mac::Address  src, dst;
-    uint16_t      fcf;
-    bool          iePresent;
+    Mac::TxFrame  *frame = nullptr;
+    Mac::Addresses addresses;
+    Mac::PanIds    panIds;
 
 #if OPENTHREAD_CONFIG_MULTI_RADIO
     Mac::RadioType radio;
 
-    SuccessOrExit(GetPollDestinationAddress(dst, radio));
+    SuccessOrExit(GetPollDestinationAddress(addresses.mDestination, radio));
     frame = &aTxFrames.GetTxFrame(radio);
 #else
-    SuccessOrExit(GetPollDestinationAddress(dst));
+    SuccessOrExit(GetPollDestinationAddress(addresses.mDestination));
     frame = &aTxFrames.GetTxFrame();
 #endif
 
-    fcf = Mac::Frame::kFcfFrameMacCmd | Mac::Frame::kFcfPanidCompression | Mac::Frame::kFcfAckRequest |
-          Mac::Frame::kFcfSecurityEnabled;
-
-    iePresent = Get<MeshForwarder>().CalcIePresent(nullptr);
-
-    if (iePresent)
+    if (addresses.mDestination.IsExtended())
     {
-        fcf |= Mac::Frame::kFcfIePresent;
-    }
-
-    fcf |= Get<MeshForwarder>().CalcFrameVersion(Get<NeighborTable>().FindNeighbor(dst), iePresent);
-
-    if (dst.IsExtended())
-    {
-        fcf |= Mac::Frame::kFcfDstAddrExt | Mac::Frame::kFcfSrcAddrExt;
-        src.SetExtended(Get<Mac::Mac>().GetExtAddress());
+        addresses.mSource.SetExtended(Get<Mac::Mac>().GetExtAddress());
     }
     else
     {
-        fcf |= Mac::Frame::kFcfDstAddrShort | Mac::Frame::kFcfSrcAddrShort;
-        src.SetShort(Get<Mac::Mac>().GetShortAddress());
+        addresses.mSource.SetShort(Get<Mac::Mac>().GetShortAddress());
     }
 
-    frame->InitMacHeader(fcf, Mac::Frame::kKeyIdMode1 | Mac::Frame::kSecEncMic32);
+    panIds.mSource      = Get<Mac::Mac>().GetPanId();
+    panIds.mDestination = Get<Mac::Mac>().GetPanId();
 
-    if (frame->IsDstPanIdPresent())
-    {
-        frame->SetDstPanId(Get<Mac::Mac>().GetPanId());
-    }
+    Get<MeshForwarder>().PrepareMacHeaders(*frame, Mac::Frame::kTypeMacCmd, addresses, panIds,
+                                           Mac::Frame::kSecurityEncMic32, Mac::Frame::kKeyIdMode1, nullptr);
 
-    frame->SetSrcAddr(src);
-    frame->SetDstAddr(dst);
-#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
-    if (iePresent)
-    {
-        Get<MeshForwarder>().AppendHeaderIe(nullptr, *frame);
-    }
-
-#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT && OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
     if (frame->GetHeaderIe(Mac::CslIe::kHeaderIeId) != nullptr)
     {
         // Disable frame retransmission when the data poll has CSL IE included
         aTxFrames.SetMaxFrameRetries(0);
     }
 #endif
-#endif
 
     IgnoreError(frame->SetCommandId(Mac::Frame::kMacCmdDataRequest));
 
diff --git a/src/core/mac/data_poll_sender.hpp b/src/core/mac/data_poll_sender.hpp
index a30f563..2327bd9 100644
--- a/src/core/mac/data_poll_sender.hpp
+++ b/src/core/mac/data_poll_sender.hpp
@@ -276,24 +276,26 @@
     void            ScheduleNextPoll(PollPeriodSelector aPollPeriodSelector);
     uint32_t        CalculatePollPeriod(void) const;
     const Neighbor &GetParent(void) const;
-    static void     HandlePollTimer(Timer &aTimer);
+    void            HandlePollTimer(void) { IgnoreError(SendDataPoll()); }
 #if OPENTHREAD_CONFIG_MULTI_RADIO
     Error GetPollDestinationAddress(Mac::Address &aDest, Mac::RadioType &aRadioType) const;
 #else
     Error GetPollDestinationAddress(Mac::Address &aDest) const;
 #endif
 
+    using PollTimer = TimerMilliIn<DataPollSender, &DataPollSender::HandlePollTimer>;
+
     TimeMilli mTimerStartTime;
     uint32_t  mPollPeriod;
     uint32_t  mExternalPollPeriod : 26; // In milliseconds.
     uint8_t   mFastPollsUsers : 6;      // Number of callers which request fast polls.
 
-    TimerMilli mTimer;
+    PollTimer mTimer;
 
     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/link_raw.cpp b/src/core/mac/link_raw.cpp
index bbce9f2..ca87f41 100644
--- a/src/core/mac/link_raw.cpp
+++ b/src/core/mac/link_raw.cpp
@@ -258,12 +258,12 @@
     return error;
 }
 
-Error LinkRaw::SetMacFrameCounter(uint32_t aMacFrameCounter)
+Error LinkRaw::SetMacFrameCounter(uint32_t aFrameCounter, bool aSetIfLarger)
 {
     Error error = kErrorNone;
 
     VerifyOrExit(IsEnabled(), error = kErrorInvalidState);
-    mSubMac.SetFrameCounter(aMacFrameCounter);
+    mSubMac.SetFrameCounter(aFrameCounter, aSetIfLarger);
 
 exit:
     return error;
@@ -274,7 +274,7 @@
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
 void LinkRaw::RecordFrameTransmitStatus(const TxFrame &aFrame,
-                                        const RxFrame *aAckFrame,
+                                        RxFrame       *aAckFrame,
                                         Error          aError,
                                         uint8_t        aRetryCount,
                                         bool           aWillRetx)
diff --git a/src/core/mac/link_raw.hpp b/src/core/mac/link_raw.hpp
index e0a66c9..d630ebe 100644
--- a/src/core/mac/link_raw.hpp
+++ b/src/core/mac/link_raw.hpp
@@ -271,13 +271,15 @@
     /**
      * This method sets the current MAC frame counter value.
      *
-     * @param[in] aMacFrameCounter  The MAC frame counter value.
+     * @param[in] aFrameCounter  The MAC frame counter value.
+     * @param[in] aSetIfLarger   If `true`, set only if the new value @p aFrameCounter is larger than current value.
+     *                           If `false`, set the new value independent of the current value.
      *
      * @retval kErrorNone            If successful.
      * @retval kErrorInvalidState    If the raw link-layer isn't enabled.
      *
      */
-    Error SetMacFrameCounter(uint32_t aMacFrameCounter);
+    Error SetMacFrameCounter(uint32_t aFrameCounter, bool aSetIfLarger);
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
     /**
@@ -298,12 +300,12 @@
      *
      */
     void RecordFrameTransmitStatus(const TxFrame &aFrame,
-                                   const RxFrame *aAckFrame,
+                                   RxFrame       *aAckFrame,
                                    Error          aError,
                                    uint8_t        aRetryCount,
                                    bool           aWillRetx);
 #else
-    void    RecordFrameTransmitStatus(const TxFrame &, const RxFrame *, Error, uint8_t, bool) {}
+    void    RecordFrameTransmitStatus(const TxFrame &, RxFrame *, Error, uint8_t, bool) {}
 #endif
 
 private:
diff --git a/src/core/mac/mac.cpp b/src/core/mac/mac.cpp
index 8ad0518..a29ee3f 100644
--- a/src/core/mac/mac.cpp
+++ b/src/core/mac/mac.cpp
@@ -92,16 +92,16 @@
 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
     , mCslTxFireTime(TimeMilli::kMaxDuration)
 #endif
+#endif
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
     , mCslChannel(0)
     , mCslPeriod(0)
 #endif
-#endif
     , mActiveScanHandler(nullptr) // Initialize `mActiveScanHandler` and `mEnergyScanHandler` union
     , mScanHandlerContext(nullptr)
     , mLinks(aInstance)
-    , mOperationTask(aInstance, Mac::HandleOperationTask)
-    , mTimer(aInstance, Mac::HandleTimer)
+    , mOperationTask(aInstance)
+    , mTimer(aInstance)
     , mKeyIdMode2FrameCounter(0)
     , mCcaSampleCount(0)
 #if OPENTHREAD_CONFIG_MULTI_RADIO
@@ -216,7 +216,7 @@
     Address address;
 #if OPENTHREAD_CONFIG_MAC_BEACON_PAYLOAD_PARSING_ENABLE
     const BeaconPayload *beaconPayload = nullptr;
-    const Beacon *       beacon        = nullptr;
+    const Beacon        *beacon        = nullptr;
     uint16_t             payloadLength;
 #endif
 
@@ -224,7 +224,7 @@
 
     VerifyOrExit(aBeaconFrame != nullptr, error = kErrorInvalidArgs);
 
-    VerifyOrExit(aBeaconFrame->GetType() == Frame::kFcfFrameBeacon, error = kErrorParse);
+    VerifyOrExit(aBeaconFrame->GetType() == Frame::kTypeBeacon, error = kErrorParse);
     SuccessOrExit(error = aBeaconFrame->GetSrcAddr(address));
     VerifyOrExit(address.IsExtended(), error = kErrorParse);
     aResult.mExtAddress = address.GetExtended();
@@ -348,7 +348,7 @@
 {
     EnergyScanResult result;
 
-    VerifyOrExit((mEnergyScanHandler != nullptr) && (aRssi != kInvalidRssiValue));
+    VerifyOrExit((mEnergyScanHandler != nullptr) && (aRssi != Radio::kInvalidRssi));
 
     result.mChannel = mScanChannel;
     result.mMaxRssi = aRssi;
@@ -571,17 +571,14 @@
     else
     {
         mLinks.Receive(mRadioChannel);
-        LogDebg("Idle mode: Radio receiving on channel %d", mRadioChannel);
+        LogDebg("Idle mode: Radio receiving on channel %u", mRadioChannel);
     }
 
 exit:
     return;
 }
 
-bool Mac::IsActiveOrPending(Operation aOperation) const
-{
-    return (mOperation == aOperation) || IsPending(aOperation);
-}
+bool Mac::IsActiveOrPending(Operation aOperation) const { return (mOperation == aOperation) || IsPending(aOperation); }
 
 void Mac::StartOperation(Operation aOperation)
 {
@@ -608,11 +605,6 @@
     }
 }
 
-void Mac::HandleOperationTask(Tasklet &aTasklet)
-{
-    aTasklet.Get<Mac>().PerformNextOperation();
-}
-
 void Mac::PerformNextOperation(void)
 {
     VerifyOrExit(mOperation == kOperationIdle);
@@ -727,12 +719,16 @@
 
 TxFrame *Mac::PrepareBeaconRequest(void)
 {
-    TxFrame &frame = mLinks.GetTxFrames().GetBroadcastTxFrame();
-    uint16_t fcf   = Frame::kFcfFrameMacCmd | Frame::kFcfDstAddrShort | Frame::kFcfSrcAddrNone;
+    TxFrame  &frame = mLinks.GetTxFrames().GetBroadcastTxFrame();
+    Addresses addrs;
+    PanIds    panIds;
 
-    frame.InitMacHeader(fcf, Frame::kSecNone);
-    frame.SetDstPanId(kShortAddrBroadcast);
-    frame.SetDstAddr(kShortAddrBroadcast);
+    addrs.mSource.SetNone();
+    addrs.mDestination.SetShort(kShortAddrBroadcast);
+    panIds.mDestination = kShortAddrBroadcast;
+
+    frame.InitMacHeader(Frame::kTypeMacCmd, Frame::kVersion2003, addrs, panIds, Frame::kSecurityNone);
+
     IgnoreError(frame.SetCommandId(Frame::kMacCmdBeaconRequest));
 
     LogInfo("Sending Beacon Request");
@@ -742,9 +738,10 @@
 
 TxFrame *Mac::PrepareBeacon(void)
 {
-    TxFrame *frame;
-    uint16_t fcf;
-    Beacon * beacon = nullptr;
+    TxFrame  *frame;
+    Beacon   *beacon = nullptr;
+    Addresses addrs;
+    PanIds    panIds;
 #if OPENTHREAD_CONFIG_MAC_OUTGOING_BEACON_PAYLOAD_ENABLE
     uint8_t        beaconLength;
     BeaconPayload *beaconPayload = nullptr;
@@ -758,10 +755,11 @@
     frame = &mLinks.GetTxFrames().GetBroadcastTxFrame();
 #endif
 
-    fcf = Frame::kFcfFrameBeacon | Frame::kFcfDstAddrNone | Frame::kFcfSrcAddrExt;
-    frame->InitMacHeader(fcf, Frame::kSecNone);
-    IgnoreError(frame->SetSrcPanId(mPanId));
-    frame->SetSrcAddr(GetExtAddress());
+    addrs.mSource.SetExtended(GetExtAddress());
+    panIds.mSource = mPanId;
+    addrs.mDestination.SetNone();
+
+    frame->InitMacHeader(Frame::kTypeBeacon, Frame::kVersion2003, addrs, panIds, Frame::kSecurityNone);
 
     beacon = reinterpret_cast<Beacon *>(frame->GetPayload());
     beacon->Init();
@@ -832,7 +830,7 @@
 
 void Mac::ProcessTransmitSecurity(TxFrame &aFrame)
 {
-    KeyManager &      keyManager = Get<KeyManager>();
+    KeyManager       &keyManager = Get<KeyManager>();
     uint8_t           keyIdMode;
     const ExtAddress *extAddress = nullptr;
 
@@ -920,7 +918,7 @@
 
 void Mac::BeginTransmit(void)
 {
-    TxFrame * frame    = nullptr;
+    TxFrame  *frame    = nullptr;
     TxFrames &txFrames = mLinks.GetTxFrames();
     Address   dstAddr;
 
@@ -1033,10 +1031,8 @@
         // copy the frame into correct `TxFrame` for each radio type
         // (if it is not already prepared).
 
-        for (uint8_t index = 0; index < GetArrayLength(RadioTypes::kAllRadioTypes); index++)
+        for (RadioType radio : RadioTypes::kAllRadioTypes)
         {
-            RadioType radio = RadioTypes::kAllRadioTypes[index];
-
             if (txFrames.GetSelectedRadioTypes().Contains(radio))
             {
                 TxFrame &txFrame = txFrames.GetTxFrame(radio);
@@ -1052,10 +1048,8 @@
         // process security for each radio type separately. This
         // allows radio links to handle security differently, e.g.,
         // with different keys or link frame counters.
-        for (uint8_t index = 0; index < GetArrayLength(RadioTypes::kAllRadioTypes); index++)
+        for (RadioType radio : RadioTypes::kAllRadioTypes)
         {
-            RadioType radio = RadioTypes::kAllRadioTypes[index];
-
             if (txFrames.GetSelectedRadioTypes().Contains(radio))
             {
                 ProcessTransmitSecurity(txFrames.GetTxFrame(radio));
@@ -1136,7 +1130,7 @@
 }
 
 void Mac::RecordFrameTransmitStatus(const TxFrame &aFrame,
-                                    const RxFrame *aAckFrame,
+                                    RxFrame       *aAckFrame,
                                     Error          aError,
                                     uint8_t        aRetryCount,
                                     bool           aWillRetx)
@@ -1199,14 +1193,20 @@
 
     if ((aError == kErrorNone) && ackRequested && (aAckFrame != nullptr) && (neighbor != nullptr))
     {
+#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
+        SuccessOrExit(mFilter.ApplyToRxFrame(*aAckFrame, neighbor->GetExtAddress(), neighbor));
+#endif
+
         neighbor->GetLinkInfo().AddRss(aAckFrame->GetRssi());
 #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
-        if (aAckFrame->GetVersion() == Frame::kFcfFrameVersion2015)
+        if (aAckFrame->GetVersion() == Frame::kVersion2015)
         {
 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
             ProcessCsl(*aAckFrame, dstAddr);
@@ -1307,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
@@ -1332,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));
@@ -1453,11 +1453,6 @@
     return;
 }
 
-void Mac::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Mac>().HandleTimer();
-}
-
 void Mac::HandleTimer(void)
 {
     switch (mOperation)
@@ -1500,7 +1495,7 @@
 
 Error Mac::ProcessReceiveSecurity(RxFrame &aFrame, const Address &aSrcAddr, Neighbor *aNeighbor)
 {
-    KeyManager &       keyManager = Get<KeyManager>();
+    KeyManager        &keyManager = Get<KeyManager>();
     Error              error      = kErrorSecurity;
     uint8_t            securityLevel;
     uint8_t            keyIdMode;
@@ -1508,15 +1503,15 @@
     uint8_t            keyid;
     uint32_t           keySequence = 0;
     const KeyMaterial *macKey;
-    const ExtAddress * extAddress;
+    const ExtAddress  *extAddress;
 
     VerifyOrExit(aFrame.GetSecurityEnabled(), error = kErrorNone);
 
     IgnoreError(aFrame.GetSecurityLevel(securityLevel));
-    VerifyOrExit(securityLevel == Frame::kSecEncMic32);
+    VerifyOrExit(securityLevel == Frame::kSecurityEncMic32);
 
     IgnoreError(aFrame.GetFrameCounter(frameCounter));
-    LogDebg("Rx security - frame counter %u", frameCounter);
+    LogDebg("Rx security - frame counter %lu", ToUlong(frameCounter));
 
     IgnoreError(aFrame.GetKeyIdMode(keyIdMode));
 
@@ -1643,15 +1638,15 @@
     uint32_t           frameCounter;
     Address            srcAddr;
     Address            dstAddr;
-    Neighbor *         neighbor   = nullptr;
-    KeyManager &       keyManager = Get<KeyManager>();
+    Neighbor          *neighbor   = nullptr;
+    KeyManager        &keyManager = Get<KeyManager>();
     const KeyMaterial *macKey;
 
     VerifyOrExit(aAckFrame.GetSecurityEnabled(), error = kErrorNone);
     VerifyOrExit(aAckFrame.IsVersion2015());
 
     IgnoreError(aAckFrame.GetSecurityLevel(securityLevel));
-    VerifyOrExit(securityLevel == Frame::kSecEncMic32);
+    VerifyOrExit(securityLevel == Frame::kSecurityEncMic32);
 
     IgnoreError(aAckFrame.GetKeyIdMode(keyIdMode));
     VerifyOrExit(keyIdMode == Frame::kKeyIdMode1, error = kErrorNone);
@@ -1662,7 +1657,7 @@
     VerifyOrExit(txKeyId == ackKeyId);
 
     IgnoreError(aAckFrame.GetFrameCounter(frameCounter));
-    LogDebg("Rx security - Ack frame counter %u", frameCounter);
+    LogDebg("Rx security - Ack frame counter %lu", ToUlong(frameCounter));
 
     IgnoreError(aAckFrame.GetSrcAddr(srcAddr));
 
@@ -1750,7 +1745,7 @@
 
     IgnoreError(aFrame->GetSrcAddr(srcaddr));
     IgnoreError(aFrame->GetDstAddr(dstaddr));
-    neighbor = Get<NeighborTable>().FindNeighbor(srcaddr);
+    neighbor = !srcaddr.IsNone() ? Get<NeighborTable>().FindNeighbor(srcaddr) : nullptr;
 
     // Destination Address Filtering
     switch (dstaddr.GetType())
@@ -1804,23 +1799,7 @@
         VerifyOrExit(srcaddr.GetExtended() != GetExtAddress(), error = kErrorInvalidSourceAddress);
 
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
-        {
-            int8_t fixedRss;
-
-            SuccessOrExit(error = mFilter.Apply(srcaddr.GetExtended(), fixedRss));
-
-            if (fixedRss != Filter::kFixedRssDisabled)
-            {
-                aFrame->SetRssi(fixedRss);
-
-                // Clear any previous link info to ensure the fixed RSSI
-                // value takes effect quickly.
-                if (neighbor != nullptr)
-                {
-                    neighbor->GetLinkInfo().Clear();
-                }
-            }
-        }
+        SuccessOrExit(error = mFilter.ApplyToRxFrame(*aFrame, srcaddr.GetExtended(), neighbor));
 #endif
 
         break;
@@ -1866,7 +1845,7 @@
     }
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
-    if (aFrame->GetVersion() == Frame::kFcfFrameVersion2015)
+    if (aFrame->GetVersion() == Frame::kVersion2015)
     {
         ProcessCsl(*aFrame, srcaddr);
     }
@@ -1907,7 +1886,7 @@
 
 #if OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2 && OPENTHREAD_FTD
                 // From Thread 1.2, MAC Data Frame can also act as keep-alive message if child supports
-                if (aFrame->GetType() == Frame::kFcfFrameData && !neighbor->IsRxOnWhenIdle() &&
+                if (aFrame->GetType() == Frame::kTypeData && !neighbor->IsRxOnWhenIdle() &&
                     neighbor->IsEnhancedKeepAliveSupported())
                 {
                     neighbor->SetLastHeard(TimerMilli::GetNow());
@@ -1925,7 +1904,7 @@
     {
     case kOperationActiveScan:
 
-        if (aFrame->GetType() == Frame::kFcfFrameBeacon)
+        if (aFrame->GetType() == Frame::kTypeBeacon)
         {
             mCounters.mRxBeacon++;
             ReportActiveScanResult(aFrame);
@@ -1970,7 +1949,7 @@
 
     switch (aFrame->GetType())
     {
-    case Frame::kFcfFrameMacCmd:
+    case Frame::kTypeMacCmd:
         if (HandleMacCommand(*aFrame)) // returns `true` when handled
         {
             ExitNow(error = kErrorNone);
@@ -1978,11 +1957,11 @@
 
         break;
 
-    case Frame::kFcfFrameBeacon:
+    case Frame::kTypeBeacon:
         mCounters.mRxBeacon++;
         break;
 
-    case Frame::kFcfFrameData:
+    case Frame::kTypeData:
         mCounters.mRxData++;
         break;
 
@@ -2128,12 +2107,11 @@
 }
 #endif
 
-void Mac::ResetRetrySuccessHistogram()
-{
-    memset(&mRetryHistogram, 0, sizeof(mRetryHistogram));
-}
+void Mac::ResetRetrySuccessHistogram() { memset(&mRetryHistogram, 0, sizeof(mRetryHistogram)); }
 #endif // OPENTHREAD_CONFIG_MAC_RETRY_SUCCESS_HISTOGRAM_ENABLE
 
+uint8_t Mac::ComputeLinkMargin(int8_t aRss) const { return ot::ComputeLinkMargin(GetNoiseFloor(), aRss); }
+
 // LCOV_EXCL_START
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
@@ -2214,7 +2192,7 @@
         uint8_t maxAttempts = aFrame.GetMaxFrameRetries() + 1;
         uint8_t curAttempt  = aWillRetx ? (aRetryCount + 1) : maxAttempts;
 
-        LogInfo("Frame tx attempt %d/%d failed, error:%s, %s", curAttempt, maxAttempts, ErrorToString(aError),
+        LogInfo("Frame tx attempt %u/%u failed, error:%s, %s", curAttempt, maxAttempts, ErrorToString(aError),
                 aFrame.ToInfoString().AsCString());
     }
     else
@@ -2223,24 +2201,15 @@
     }
 }
 
-void Mac::LogBeacon(const char *aActionText) const
-{
-    LogInfo("%s Beacon", aActionText);
-}
+void Mac::LogBeacon(const char *aActionText) const { LogInfo("%s Beacon", aActionText); }
 
 #else // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
-void Mac::LogFrameRxFailure(const RxFrame *, Error) const
-{
-}
+void Mac::LogFrameRxFailure(const RxFrame *, Error) const {}
 
-void Mac::LogBeacon(const char *) const
-{
-}
+void Mac::LogBeacon(const char *) const {}
 
-void Mac::LogFrameTxFailure(const TxFrame &, Error, uint8_t, bool) const
-{
-}
+void Mac::LogFrameTxFailure(const TxFrame &, Error, uint8_t, bool) const {}
 
 #endif // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
@@ -2299,15 +2268,9 @@
     UpdateCsl();
 }
 
-bool Mac::IsCslEnabled(void) const
-{
-    return !Get<Mle::Mle>().IsRxOnWhenIdle() && IsCslCapable();
-}
+bool Mac::IsCslEnabled(void) const { return !Get<Mle::Mle>().IsRxOnWhenIdle() && IsCslCapable(); }
 
-bool Mac::IsCslCapable(void) const
-{
-    return (GetCslPeriod() > 0) && IsCslSupported();
-}
+bool Mac::IsCslCapable(void) const { return (GetCslPeriod() > 0) && IsCslSupported(); }
 
 bool Mac::IsCslSupported(void) const
 {
@@ -2319,8 +2282,8 @@
 void Mac::ProcessCsl(const RxFrame &aFrame, const Address &aSrcAddr)
 {
     const uint8_t *cur   = aFrame.GetHeaderIe(CslIe::kHeaderIeId);
-    Child *        child = Get<ChildTable>().FindChild(aSrcAddr, Child::kInStateAnyExceptInvalid);
-    const CslIe *  csl;
+    Child         *child = Get<ChildTable>().FindChild(aSrcAddr, Child::kInStateAnyExceptInvalid);
+    const CslIe   *csl;
 
     VerifyOrExit(cur != nullptr && child != nullptr && aFrame.GetSecurityEnabled());
 
@@ -2332,9 +2295,9 @@
     child->SetCslSynchronized(true);
     child->SetCslLastHeard(TimerMilli::GetNow());
     child->SetLastRxTimestamp(aFrame.GetTimestamp());
-    LogDebg("Timestamp=%u Sequence=%u CslPeriod=%hu CslPhase=%hu TransmitPhase=%hu",
-            static_cast<uint32_t>(aFrame.GetTimestamp()), aFrame.GetSequence(), csl->GetPeriod(), csl->GetPhase(),
-            child->GetCslPhase());
+    LogDebg("Timestamp=%lu Sequence=%u CslPeriod=%u CslPhase=%u TransmitPhase=%u",
+            ToUlong(static_cast<uint32_t>(aFrame.GetTimestamp())), aFrame.GetSequence(), csl->GetPeriod(),
+            csl->GetPhase(), child->GetCslPhase());
 
     Get<CslTxScheduler>().Update();
 
@@ -2343,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;
@@ -2359,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 babdbbf..3566109 100644
--- a/src/core/mac/mac.hpp
+++ b/src/core/mac/mac.hpp
@@ -71,8 +71,9 @@
 
 namespace Mac {
 
-constexpr uint32_t kDataPollTimeout = 100; ///< Timeout for receiving Data Frame (in msec).
-constexpr uint32_t kSleepDelay      = 300; ///< Max sleep delay when frame is pending (in msec).
+constexpr uint32_t kDataPollTimeout =
+    OPENTHREAD_CONFIG_MAC_DATA_POLL_TIMEOUT; ///< Timeout for receiving Data Frame (in msec).
+constexpr uint32_t kSleepDelay = 300;        ///< Max sleep delay when frame is pending (in msec)
 
 constexpr uint16_t kScanDurationDefault = OPENTHREAD_CONFIG_MAC_SCAN_DURATION; ///< Duration per channel (in msec).
 
@@ -412,7 +413,7 @@
      *
      */
     void RecordFrameTransmitStatus(const TxFrame &aFrame,
-                                   const RxFrame *aAckFrame,
+                                   RxFrame       *aAckFrame,
                                    Error          aError,
                                    uint8_t        aRetryCount,
                                    bool           aWillRetx);
@@ -546,7 +547,17 @@
      * @returns The noise floor value in dBm.
      *
      */
-    int8_t GetNoiseFloor(void) { return mLinks.GetNoiseFloor(); }
+    int8_t GetNoiseFloor(void) const { return mLinks.GetNoiseFloor(); }
+
+    /**
+     * This method computes the link margin for a given a received signal strength value using noise floor.
+     *
+     * @param[in] aRss The received signal strength in dBm.
+     *
+     * @returns The link margin for @p aRss in dB based on noise floor.
+     *
+     */
+    uint8_t ComputeLinkMargin(int8_t aRss) const;
 
     /**
      * This method returns the current CCA (Clear Channel Assessment) failure rate.
@@ -576,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.
@@ -608,6 +625,14 @@
     uint16_t GetCslPeriod(void) const { return mCslPeriod; }
 
     /**
+     * This method gets the CSL period.
+     *
+     * @returns CSL period in milliseconds.
+     *
+     */
+    uint32_t GetCslPeriodMs(void) const { return mCslPeriod * kUsPerTenSymbols / 1000; }
+
+    /**
      * This method sets the CSL period.
      *
      * @param[in]  aPeriod  The CSL period in 10 symbols.
@@ -643,42 +668,24 @@
     bool IsCslSupported(void) const;
 
     /**
-     * This method returns CSL parent clock accuracy, in ± ppm.
+     * This method returns parent CSL accuracy (clock accuracy and uncertainty).
      *
-     * @retval CSL parent clock accuracy, in ± ppm.
+     * @returns The parent CSL accuracy.
      *
      */
-    uint8_t GetCslParentClockAccuracy(void) const { return mLinks.GetSubMac().GetCslParentClockAccuracy(); }
+    const CslAccuracy &GetCslParentAccuracy(void) const { return mLinks.GetSubMac().GetCslParentAccuracy(); }
 
     /**
-     * This method sets CSL parent clock accuracy, in ± ppm.
+     * This method sets parent CSL accuracy.
      *
-     * @param[in] aCslParentAccuracy CSL parent clock accuracy, in ± ppm.
+     * @param[in] aCslAccuracy  The parent CSL accuracy.
      *
      */
-    void SetCslParentClockAccuracy(uint8_t aCslParentAccuracy)
+    void SetCslParentAccuracy(const CslAccuracy &aCslAccuracy)
     {
-        mLinks.GetSubMac().SetCslParentClockAccuracy(aCslParentAccuracy);
+        mLinks.GetSubMac().SetCslParentAccuracy(aCslAccuracy);
     }
 
-    /**
-     * This method returns CSL parent uncertainty, in ±10 us units.
-     *
-     * @retval CSL parent uncertainty, in ±10 us units.
-     *
-     */
-    uint8_t GetCslParentUncertainty(void) const { return mLinks.GetSubMac().GetCslParentUncertainty(); }
-
-    /**
-     * This method returns CSL parent uncertainty, in ±10 us units.
-     *
-     * @param[in] aCslParentUncert  CSL parent uncertainty, in ±10 us units.
-     *
-     */
-    void SetCslParentUncertainty(uint8_t aCslParentUncert)
-    {
-        mLinks.GetSubMac().SetCslParentUncertainty(aCslParentUncert);
-    }
 #endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE && OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
@@ -705,7 +712,6 @@
 #endif
 
 private:
-    static constexpr int8_t   kInvalidRssiValue  = SubMac::kInvalidRssiValue;
     static constexpr uint16_t kMaxCcaSampleCount = OPENTHREAD_CONFIG_CCA_FAILURE_RATE_AVERAGING_WINDOW;
 
     enum Operation : uint8_t
@@ -768,10 +774,7 @@
     bool     IsJoinable(void) const;
     void     BeginTransmit(void);
     bool     HandleMacCommand(RxFrame &aFrame);
-
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
-    static void HandleOperationTask(Tasklet &aTasklet);
+    void     HandleTimer(void);
 
     void  Scan(Operation aScanOperation, uint32_t aScanChannels, uint16_t aScanDuration);
     Error UpdateScanChannel(void);
@@ -792,11 +795,14 @@
 #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);
 
+    using OperationTask = TaskletIn<Mac, &Mac::PerformNextOperation>;
+    using MacTimer      = TimerMilliIn<Mac, &Mac::HandleTimer>;
+
     static const otExtAddress sMode2ExtAddress;
 
     bool mEnabled : 1;
@@ -843,8 +849,8 @@
     void *mScanHandlerContext;
 
     Links              mLinks;
-    Tasklet            mOperationTask;
-    TimerMilli         mTimer;
+    OperationTask      mOperationTask;
+    MacTimer           mTimer;
     otMacCounters      mCounters;
     uint32_t           mKeyIdMode2FrameCounter;
     SuccessRateTracker mCcaSuccessRateTracker;
diff --git a/src/core/mac/mac_filter.cpp b/src/core/mac/mac_filter.cpp
index c119c42..97604a3 100644
--- a/src/core/mac/mac_filter.cpp
+++ b/src/core/mac/mac_filter.cpp
@@ -38,6 +38,7 @@
 #include "common/array.hpp"
 #include "common/as_core_type.hpp"
 #include "common/code_utils.hpp"
+#include "thread/topology.hpp"
 
 namespace ot {
 namespace Mac {
@@ -53,11 +54,11 @@
     }
 }
 
-Filter::FilterEntry *Filter::FindEntry(const ExtAddress &aExtAddress)
+const Filter::FilterEntry *Filter::FindEntry(const ExtAddress &aExtAddress) const
 {
-    FilterEntry *rval = nullptr;
+    const FilterEntry *rval = nullptr;
 
-    for (FilterEntry &entry : mFilterEntries)
+    for (const FilterEntry &entry : mFilterEntries)
     {
         if (entry.IsInUse() && (aExtAddress == entry.mExtAddress))
         {
@@ -182,13 +183,13 @@
     mDefaultRssIn = kFixedRssDisabled;
 }
 
-Error Filter::GetNextRssIn(Iterator &aIterator, Entry &aEntry)
+Error Filter::GetNextRssIn(Iterator &aIterator, Entry &aEntry) const
 {
     Error error = kErrorNotFound;
 
     for (; aIterator < GetArrayLength(mFilterEntries); aIterator++)
     {
-        FilterEntry &entry = mFilterEntries[aIterator];
+        const FilterEntry &entry = mFilterEntries[aIterator];
 
         if (entry.mRssIn != kFixedRssDisabled)
         {
@@ -213,11 +214,11 @@
     return error;
 }
 
-Error Filter::Apply(const ExtAddress &aExtAddress, int8_t &aRss)
+Error Filter::Apply(const ExtAddress &aExtAddress, int8_t &aRss) const
 {
-    Error        error = kErrorNone;
-    FilterEntry *entry = FindEntry(aExtAddress);
-    bool         isInFilterList;
+    Error              error = kErrorNone;
+    const FilterEntry *entry = FindEntry(aExtAddress);
+    bool               isInFilterList;
 
     // Use the default RssIn setting for all receiving messages first.
     aRss = mDefaultRssIn;
@@ -250,6 +251,28 @@
     return error;
 }
 
+Error Filter::ApplyToRxFrame(RxFrame &aRxFrame, const ExtAddress &aExtAddress, Neighbor *aNeighbor) const
+{
+    Error  error;
+    int8_t fixedRss;
+
+    SuccessOrExit(error = Apply(aExtAddress, fixedRss));
+
+    VerifyOrExit(fixedRss != kFixedRssDisabled);
+
+    aRxFrame.SetRssi(fixedRss);
+
+    if (aNeighbor != nullptr)
+    {
+        // Clear the previous RSS average to ensure the fixed RSS
+        // value takes effect quickly.
+        aNeighbor->GetLinkInfo().ClearAverageRss();
+    }
+
+exit:
+    return error;
+}
+
 } // namespace Mac
 } // namespace ot
 
diff --git a/src/core/mac/mac_filter.hpp b/src/core/mac/mac_filter.hpp
index 2eaef71..d5ff8bb 100644
--- a/src/core/mac/mac_filter.hpp
+++ b/src/core/mac/mac_filter.hpp
@@ -41,10 +41,14 @@
 #include <stdint.h>
 
 #include "common/as_core_type.hpp"
+#include "common/const_cast.hpp"
 #include "common/non_copyable.hpp"
 #include "mac/mac_frame.hpp"
 
 namespace ot {
+
+class Neighbor;
+
 namespace Mac {
 
 /**
@@ -208,7 +212,7 @@
      * @retval kErrorNotFound  No subsequent entry exists.
      *
      */
-    Error GetNextRssIn(Iterator &aIterator, Entry &aEntry);
+    Error GetNextRssIn(Iterator &aIterator, Entry &aEntry) const;
 
     /**
      * This method applies the filter rules on a given Extended Address.
@@ -220,7 +224,24 @@
      * @retval kErrorAddressFiltered  Address filter (allowlist or denylist) is enabled and @p aExtAddress is filtered.
      *
      */
-    Error Apply(const ExtAddress &aExtAddress, int8_t &aRss);
+    Error Apply(const ExtAddress &aExtAddress, int8_t &aRss) const;
+
+    /**
+     * This method applies the filter rules to a received frame from a given Extended Address.
+     *
+     * This method can potentially update the signal strength value on the received frame @p aRxFrame. If @p aNeighbor
+     * is not `nullptr` and filter applies a fixed RSS to the @p aRxFrame, this method will also clear the current RSS
+     * average on @p aNeighbor to ensure that the new fixed RSS takes effect quickly.
+     *
+     * @param[out] aRxFrame     The received frame.
+     * @param[in]  aExtAddress  The extended address from which @p aRxFrame was received.
+     * @param[in]  aNeighbor    A pointer to the neighbor (can be `nullptr` if not known).
+     *
+     * @retval kErrorNone             Successfully applied the filter, @p aRxFrame RSS may be updated.
+     * @retval kErrorAddressFiltered  Address filter (allowlist or denylist) is enabled and @p aExtAddress is filtered.
+     *
+     */
+    Error ApplyToRxFrame(RxFrame &aRxFrame, const ExtAddress &aExtAddress, Neighbor *aNeighbor = nullptr) const;
 
 private:
     static constexpr uint16_t kMaxEntries = OPENTHREAD_CONFIG_MAC_FILTER_SIZE;
@@ -234,8 +255,9 @@
         bool IsInUse(void) const { return mFiltered || (mRssIn != kFixedRssDisabled); }
     };
 
-    FilterEntry *FindAvailableEntry(void);
-    FilterEntry *FindEntry(const ExtAddress &aExtAddress);
+    FilterEntry       *FindAvailableEntry(void);
+    const FilterEntry *FindEntry(const ExtAddress &aExtAddress) const;
+    FilterEntry *FindEntry(const ExtAddress &aExtAddress) { return AsNonConst(AsConst(this)->FindEntry(aExtAddress)); }
 
     Mode        mMode;
     int8_t      mDefaultRssIn;
diff --git a/src/core/mac/mac_frame.cpp b/src/core/mac/mac_frame.cpp
index 2f0b1d1..a31350b 100644
--- a/src/core/mac/mac_frame.cpp
+++ b/src/core/mac/mac_frame.cpp
@@ -37,6 +37,7 @@
 
 #include "common/code_utils.hpp"
 #include "common/debug.hpp"
+#include "common/frame_builder.hpp"
 #include "common/log.hpp"
 #include "radio/trel_link.hpp"
 #if !OPENTHREAD_RADIO || OPENTHREAD_CONFIG_MAC_SOFTWARE_TX_SECURITY_ENABLE
@@ -58,23 +59,106 @@
     SetLength(aLen);
 }
 
-void Frame::InitMacHeader(uint16_t aFcf, uint8_t aSecurityControl)
+void Frame::InitMacHeader(Type             aType,
+                          Version          aVersion,
+                          const Addresses &aAddrs,
+                          const PanIds    &aPanIds,
+                          SecurityLevel    aSecurityLevel,
+                          KeyIdMode        aKeyIdMode)
 {
-    mLength = CalculateAddrFieldSize(aFcf);
+    uint16_t     fcf;
+    FrameBuilder builder;
 
-    OT_ASSERT(mLength != kInvalidSize);
+    fcf = static_cast<uint16_t>(aType) | static_cast<uint16_t>(aVersion);
 
-    WriteUint16(aFcf, mPsdu);
-
-    if (aFcf & kFcfSecurityEnabled)
+    switch (aAddrs.mSource.GetType())
     {
-        mPsdu[mLength] = aSecurityControl;
-
-        mLength += CalculateSecurityHeaderSize(aSecurityControl);
-        mLength += CalculateMicSize(aSecurityControl);
+    case Address::kTypeNone:
+        fcf |= kFcfSrcAddrNone;
+        break;
+    case Address::kTypeShort:
+        fcf |= kFcfSrcAddrShort;
+        break;
+    case Address::kTypeExtended:
+        fcf |= kFcfSrcAddrExt;
+        break;
     }
 
-    if ((aFcf & kFcfFrameTypeMask) == kFcfFrameMacCmd)
+    switch (aAddrs.mDestination.GetType())
+    {
+    case Address::kTypeNone:
+        fcf |= kFcfDstAddrNone;
+        break;
+    case Address::kTypeShort:
+        fcf |= kFcfDstAddrShort;
+        fcf |= ((aAddrs.mDestination.GetShort() == kShortAddrBroadcast) ? 0 : kFcfAckRequest);
+        break;
+    case Address::kTypeExtended:
+        fcf |= (kFcfDstAddrExt | kFcfAckRequest);
+        break;
+    }
+
+    fcf |= (aSecurityLevel != kSecurityNone) ? kFcfSecurityEnabled : 0;
+
+    // When we have both source and destination addresses we check PAN
+    // IDs to determine whether to include `kFcfPanidCompression`.
+
+    if (!aAddrs.mSource.IsNone() && !aAddrs.mDestination.IsNone() && (aPanIds.mSource == aPanIds.mDestination))
+    {
+        switch (aVersion)
+        {
+        case kVersion2015:
+            // Special case for a IEEE 802.15.4-2015 frame: When both
+            // addresses are extended, the PAN ID compression is set
+            // to one to indicate that no PAN ID is in the frame,
+            // while setting the PAN ID Compression to zero indicates
+            // the presence of the destination PAN ID in the frame.
+
+            if (aAddrs.mSource.IsExtended() && aAddrs.mDestination.IsExtended())
+            {
+                break;
+            }
+
+            OT_FALL_THROUGH;
+
+        case kVersion2003:
+        case kVersion2006:
+            fcf |= kFcfPanidCompression;
+            break;
+        }
+    }
+
+    builder.Init(mPsdu, GetMtu());
+    IgnoreError(builder.AppendLittleEndianUint16(fcf));
+    IgnoreError(builder.AppendUint8(0)); // Seq number
+
+    if (IsDstPanIdPresent(fcf))
+    {
+        IgnoreError(builder.AppendLittleEndianUint16(aPanIds.mDestination));
+    }
+
+    IgnoreError(builder.AppendMacAddress(aAddrs.mDestination));
+
+    if (IsSrcPanIdPresent(fcf))
+    {
+        IgnoreError(builder.AppendLittleEndianUint16(aPanIds.mSource));
+    }
+
+    IgnoreError(builder.AppendMacAddress(aAddrs.mSource));
+
+    mLength = builder.GetLength();
+
+    if (aSecurityLevel != kSecurityNone)
+    {
+        uint8_t secCtl = static_cast<uint8_t>(aSecurityLevel) | static_cast<uint8_t>(aKeyIdMode);
+
+        IgnoreError(builder.AppendUint8(secCtl));
+
+        mLength += CalculateSecurityHeaderSize(secCtl);
+        mLength += CalculateMicSize(secCtl);
+    }
+
+    if (aType == kTypeMacCmd)
     {
         mLength += kCommandIdSize;
     }
@@ -82,10 +166,7 @@
     mLength += GetFcsSize();
 }
 
-uint16_t Frame::GetFrameControlField(void) const
-{
-    return ReadUint16(mPsdu);
-}
+uint16_t Frame::GetFrameControlField(void) const { return ReadUint16(mPsdu); }
 
 Error Frame::ValidatePsdu(void) const
 {
@@ -139,7 +220,6 @@
 {
     bool present = true;
 
-#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
     if (IsVersion2015(aFcf))
     {
         switch (aFcf & (kFcfDstAddrMask | kFcfSrcAddrMask | kFcfPanidCompression))
@@ -159,7 +239,6 @@
         }
     }
     else
-#endif
     {
         present = IsDstAddrPresent(aFcf);
     }
@@ -187,10 +266,7 @@
     WriteUint16(aPanId, &mPsdu[index]);
 }
 
-uint8_t Frame::FindDstAddrIndex(void) const
-{
-    return kFcfSize + kDsnSize + (IsDstPanIdPresent() ? sizeof(PanId) : 0);
-}
+uint8_t Frame::FindDstAddrIndex(void) const { return kFcfSize + kDsnSize + (IsDstPanIdPresent() ? sizeof(PanId) : 0); }
 
 Error Frame::GetDstAddr(Address &aAddress) const
 {
@@ -283,26 +359,21 @@
 
 bool Frame::IsSrcPanIdPresent(uint16_t aFcf)
 {
-    bool srcPanIdPresent = false;
+    bool present = IsSrcAddrPresent(aFcf) && ((aFcf & kFcfPanidCompression) == 0);
 
-    if ((aFcf & kFcfSrcAddrMask) != kFcfSrcAddrNone && (aFcf & kFcfPanidCompression) == 0)
+    // Special case for a IEEE 802.15.4-2015 frame: When both
+    // addresses are extended, then the source PAN iD is not present
+    // independent of PAN ID Compression. In this case, if the PAN ID
+    // compression is set, it indicates that no PAN ID is in the
+    // frame, while if the PAN ID Compression is zero, it indicates
+    // the presence of the destination PAN ID in the frame.
+
+    if (IsVersion2015(aFcf) && ((aFcf & (kFcfDstAddrMask | kFcfSrcAddrMask)) == (kFcfDstAddrExt | kFcfSrcAddrExt)))
     {
-#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
-        // Handle a special case in IEEE 802.15.4-2015, when Pan ID Compression is 0, but Src Pan ID is not present:
-        //  Dest Address:       Extended
-        //  Source Address:     Extended
-        //  Dest Pan ID:        Present
-        //  Src Pan ID:         Not Present
-        //  Pan ID Compression: 0
-        if (!IsVersion2015(aFcf) || (aFcf & kFcfDstAddrMask) != kFcfDstAddrExt ||
-            (aFcf & kFcfSrcAddrMask) != kFcfSrcAddrExt)
-#endif
-        {
-            srcPanIdPresent = true;
-        }
+        present = false;
     }
 
-    return srcPanIdPresent;
+    return present;
 }
 
 Error Frame::GetSrcPanId(PanId &aPanId) const
@@ -378,9 +449,14 @@
         aAddress.SetExtended(&mPsdu[index], ExtAddress::kReverseByteOrder);
         break;
 
-    default:
+    case kFcfSrcAddrNone:
         aAddress.SetNone();
         break;
+
+    default:
+        // reserved value
+        error = kErrorParse;
+        break;
     }
 
 exit:
@@ -620,7 +696,7 @@
     bool    isDataRequest = false;
     uint8_t commandId;
 
-    VerifyOrExit(GetType() == kFcfFrameMacCmd);
+    VerifyOrExit(GetType() == kTypeMacCmd);
     SuccessOrExit(GetCommandId(commandId));
     isDataRequest = (commandId == kMacCmdDataRequest);
 
@@ -628,10 +704,7 @@
     return isDataRequest;
 }
 
-uint8_t Frame::GetHeaderLength(void) const
-{
-    return static_cast<uint8_t>(GetPayload() - mPsdu);
-}
+uint8_t Frame::GetHeaderLength(void) const { return static_cast<uint8_t>(GetPayload() - mPsdu); }
 
 uint8_t Frame::GetFooterLength(void) const
 {
@@ -651,23 +724,23 @@
 
     switch (aSecurityControl & kSecLevelMask)
     {
-    case kSecNone:
-    case kSecEnc:
+    case kSecurityNone:
+    case kSecurityEnc:
         micSize = kMic0Size;
         break;
 
-    case kSecMic32:
-    case kSecEncMic32:
+    case kSecurityMic32:
+    case kSecurityEncMic32:
         micSize = kMic32Size;
         break;
 
-    case kSecMic64:
-    case kSecEncMic64:
+    case kSecurityMic64:
+    case kSecurityEncMic64:
         micSize = kMic64Size;
         break;
 
-    case kSecMic128:
-    case kSecEncMic128:
+    case kSecurityMic128:
+    case kSecurityEncMic128:
         micSize = kMic128Size;
         break;
     }
@@ -675,20 +748,11 @@
     return micSize;
 }
 
-uint16_t Frame::GetMaxPayloadLength(void) const
-{
-    return GetMtu() - (GetHeaderLength() + GetFooterLength());
-}
+uint16_t Frame::GetMaxPayloadLength(void) const { return GetMtu() - (GetHeaderLength() + GetFooterLength()); }
 
-uint16_t Frame::GetPayloadLength(void) const
-{
-    return mLength - (GetHeaderLength() + GetFooterLength());
-}
+uint16_t Frame::GetPayloadLength(void) const { return mLength - (GetHeaderLength() + GetFooterLength()); }
 
-void Frame::SetPayloadLength(uint16_t aLength)
-{
-    mLength = GetHeaderLength() + GetFooterLength() + aLength;
-}
+void Frame::SetPayloadLength(uint16_t aLength) { mLength = GetHeaderLength() + GetFooterLength() + aLength; }
 
 uint8_t Frame::SkipSecurityHeaderIndex(void) const
 {
@@ -720,7 +784,7 @@
 {
     uint8_t size = kSecurityControlSize + kFrameCounterSize;
 
-    VerifyOrExit((aSecurityControl & kSecLevelMask) != kSecNone, size = kInvalidSize);
+    VerifyOrExit((aSecurityControl & kSecLevelMask) != kSecurityNone, size = kInvalidSize);
 
     switch (aSecurityControl & kKeyIdModeMask)
     {
@@ -858,7 +922,7 @@
     }
 #endif // OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
 
-    if (!IsVersion2015() && (GetFrameControlField() & kFcfFrameTypeMask) == kFcfFrameMacCmd)
+    if (!IsVersion2015() && (GetFrameControlField() & kFcfFrameTypeMask) == kTypeMacCmd)
     {
         index += kCommandIdSize;
     }
@@ -879,10 +943,7 @@
     return payload;
 }
 
-const uint8_t *Frame::GetFooter(void) const
-{
-    return mPsdu + mLength - GetFooterLength();
-}
+const uint8_t *Frame::GetFooter(void) const { return mPsdu + mLength - GetFooterLength(); }
 
 #if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
 uint8_t Frame::FindHeaderIeIndex(void) const
@@ -914,6 +975,8 @@
 {
     Error error = kErrorNone;
 
+    WriteUint16(GetFrameControlField() | kFcfIePresent, mPsdu);
+
     if (aIndex == 0)
     {
         aIndex = FindHeaderIeIndex();
@@ -938,16 +1001,10 @@
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-template <> void Frame::InitIeContentAt<CslIe>(uint8_t &aIndex)
-{
-    aIndex += sizeof(CslIe);
-}
+template <> void Frame::InitIeContentAt<CslIe>(uint8_t &aIndex) { aIndex += sizeof(CslIe); }
 #endif
 
-template <> void Frame::InitIeContentAt<Termination2Ie>(uint8_t &aIndex)
-{
-    OT_UNUSED_VARIABLE(aIndex);
-}
+template <> void Frame::InitIeContentAt<Termination2Ie>(uint8_t &aIndex) { OT_UNUSED_VARIABLE(aIndex); }
 #endif // OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
 
 const uint8_t *Frame::GetHeaderIe(uint8_t aIeId) const
@@ -1018,7 +1075,7 @@
 void Frame::SetCslIe(uint16_t aCslPeriod, uint16_t aCslPhase)
 {
     uint8_t *cur = GetHeaderIe(CslIe::kHeaderIeId);
-    CslIe *  csl;
+    CslIe   *csl;
 
     VerifyOrExit(cur != nullptr);
 
@@ -1044,7 +1101,7 @@
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 const TimeIe *Frame::GetTimeIe(void) const
 {
-    const TimeIe * timeIe = nullptr;
+    const TimeIe  *timeIe = nullptr;
     const uint8_t *cur    = nullptr;
 
     cur = GetHeaderIe(VendorIeHeader::kHeaderIeId);
@@ -1107,15 +1164,9 @@
 }
 
 #elif OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
-uint16_t Frame::GetMtu(void) const
-{
-    return Trel::Link::kMtuSize;
-}
+uint16_t Frame::GetMtu(void) const { return Trel::Link::kMtuSize; }
 
-uint8_t Frame::GetFcsSize(void) const
-{
-    return Trel::Link::kFcsSize;
-}
+uint8_t Frame::GetFcsSize(void) const { return Trel::Link::kFcsSize; }
 #endif
 
 // Explicit instantiation
@@ -1131,7 +1182,7 @@
 
 void TxFrame::CopyFrom(const TxFrame &aFromFrame)
 {
-    uint8_t *      psduBuffer   = mPsdu;
+    uint8_t       *psduBuffer   = mPsdu;
     otRadioIeInfo *ieInfoBuffer = mInfo.mTxInfo.mIeInfo;
 #if OPENTHREAD_CONFIG_MULTI_RADIO
     uint8_t radioType = mRadioType;
@@ -1203,7 +1254,7 @@
 
 void TxFrame::GenerateImmAck(const RxFrame &aFrame, bool aIsFramePending)
 {
-    uint16_t fcf = kFcfFrameAck | aFrame.GetVersion();
+    uint16_t fcf = static_cast<uint16_t>(kTypeAck) | aFrame.GetVersion();
 
     mChannel = aFrame.mChannel;
     memset(&mInfo.mTxInfo, 0, sizeof(mInfo.mTxInfo));
@@ -1224,13 +1275,15 @@
 {
     Error error = kErrorNone;
 
-    uint16_t fcf = kFcfFrameAck | kFcfFrameVersion2015 | kFcfSrcAddrNone;
+    uint16_t fcf;
     Address  address;
     PanId    panId;
     uint8_t  footerLength;
     uint8_t  securityControlField;
     uint8_t  keyId;
 
+    fcf = static_cast<uint16_t>(kTypeAck) | static_cast<uint16_t>(kVersion2015) | kFcfSrcAddrNone;
+
     mChannel = aFrame.mChannel;
     memset(&mInfo.mTxInfo, 0, sizeof(mInfo.mTxInfo));
 
@@ -1394,19 +1447,19 @@
 
     switch (type)
     {
-    case kFcfFrameBeacon:
+    case kTypeBeacon:
         string.Append("Beacon");
         break;
 
-    case kFcfFrameData:
+    case kTypeData:
         string.Append("Data");
         break;
 
-    case kFcfFrameAck:
+    case kTypeAck:
         string.Append("Ack");
         break;
 
-    case kFcfFrameMacCmd:
+    case kTypeMacCmd:
         if (GetCommandId(commandId) != kErrorNone)
         {
             commandId = 0xff;
diff --git a/src/core/mac/mac_frame.hpp b/src/core/mac/mac_frame.hpp
index 9ffe2d5..78924bf 100644
--- a/src/core/mac/mac_frame.hpp
+++ b/src/core/mac/mac_frame.hpp
@@ -275,75 +275,81 @@
 class Frame : public otRadioFrame
 {
 public:
-    static constexpr uint8_t kFcfSize             = sizeof(uint16_t);
-    static constexpr uint8_t kDsnSize             = sizeof(uint8_t);
-    static constexpr uint8_t kSecurityControlSize = sizeof(uint8_t);
-    static constexpr uint8_t kFrameCounterSize    = sizeof(uint32_t);
-    static constexpr uint8_t kCommandIdSize       = sizeof(uint8_t);
-    static constexpr uint8_t k154FcsSize          = sizeof(uint16_t);
+    /**
+     * This enumeration represents the MAC frame type.
+     *
+     * Values match the Frame Type field in Frame Control Field (FCF)  as an `uint16_t`.
+     *
+     */
+    enum Type : uint16_t
+    {
+        kTypeBeacon = 0, ///< Beacon Frame Type.
+        kTypeData   = 1, ///< Data Frame Type.
+        kTypeAck    = 2, ///< Ack Frame Type.
+        kTypeMacCmd = 3, ///< MAC Command Frame Type.
+    };
 
-    static constexpr uint16_t kFcfFrameBeacon      = 0 << 0;
-    static constexpr uint16_t kFcfFrameData        = 1 << 0;
-    static constexpr uint16_t kFcfFrameAck         = 2 << 0;
-    static constexpr uint16_t kFcfFrameMacCmd      = 3 << 0;
-    static constexpr uint16_t kFcfFrameTypeMask    = 7 << 0;
-    static constexpr uint16_t kFcfSecurityEnabled  = 1 << 3;
-    static constexpr uint16_t kFcfFramePending     = 1 << 4;
-    static constexpr uint16_t kFcfAckRequest       = 1 << 5;
-    static constexpr uint16_t kFcfPanidCompression = 1 << 6;
-    static constexpr uint16_t kFcfIePresent        = 1 << 9;
-    static constexpr uint16_t kFcfDstAddrNone      = 0 << 10;
-    static constexpr uint16_t kFcfDstAddrShort     = 2 << 10;
-    static constexpr uint16_t kFcfDstAddrExt       = 3 << 10;
-    static constexpr uint16_t kFcfDstAddrMask      = 3 << 10;
-    static constexpr uint16_t kFcfFrameVersion2006 = 1 << 12;
-    static constexpr uint16_t kFcfFrameVersion2015 = 2 << 12;
-    static constexpr uint16_t kFcfFrameVersionMask = 3 << 12;
-    static constexpr uint16_t kFcfSrcAddrNone      = 0 << 14;
-    static constexpr uint16_t kFcfSrcAddrShort     = 2 << 14;
-    static constexpr uint16_t kFcfSrcAddrExt       = 3 << 14;
-    static constexpr uint16_t kFcfSrcAddrMask      = 3 << 14;
+    /**
+     * This enumeration represents the MAC frame version.
+     *
+     * Values match the Version field in Frame Control Field (FCF) as an `uint16_t`.
+     *
+     */
+    enum Version : uint16_t
+    {
+        kVersion2003 = 0 << 12, ///< 2003 Frame Version.
+        kVersion2006 = 1 << 12, ///< 2006 Frame Version.
+        kVersion2015 = 2 << 12, ///< 2015 Frame Version.
+    };
 
-    static constexpr uint8_t kSecNone      = 0 << 0;
-    static constexpr uint8_t kSecMic32     = 1 << 0;
-    static constexpr uint8_t kSecMic64     = 2 << 0;
-    static constexpr uint8_t kSecMic128    = 3 << 0;
-    static constexpr uint8_t kSecEnc       = 4 << 0;
-    static constexpr uint8_t kSecEncMic32  = 5 << 0;
-    static constexpr uint8_t kSecEncMic64  = 6 << 0;
-    static constexpr uint8_t kSecEncMic128 = 7 << 0;
-    static constexpr uint8_t kSecLevelMask = 7 << 0;
+    /**
+     * This enumeration represents the MAC frame security level.
+     *
+     * Values match the Security Level field in Security Control Field as an `uint8_t`.
+     *
+     */
+    enum SecurityLevel : uint8_t
+    {
+        kSecurityNone      = 0, ///< No security.
+        kSecurityMic32     = 1, ///< No encryption, MIC-32 authentication.
+        kSecurityMic64     = 2, ///< No encryption, MIC-64 authentication.
+        kSecurityMic128    = 3, ///< No encryption, MIC-128 authentication.
+        kSecurityEnc       = 4, ///< Encryption, no authentication
+        kSecurityEncMic32  = 5, ///< Encryption with MIC-32 authentication.
+        kSecurityEncMic64  = 6, ///< Encryption with MIC-64 authentication.
+        kSecurityEncMic128 = 7, ///< Encryption with MIC-128 authentication.
+    };
 
-    static constexpr uint8_t kMic0Size   = 0;
-    static constexpr uint8_t kMic32Size  = 32 / CHAR_BIT;
-    static constexpr uint8_t kMic64Size  = 64 / CHAR_BIT;
-    static constexpr uint8_t kMic128Size = 128 / CHAR_BIT;
-    static constexpr uint8_t kMaxMicSize = kMic128Size;
+    /**
+     * This enumeration represents the MAC frame security key identifier mode.
+     *
+     * Values match the Key Identifier Mode field in Security Control Field as an `uint8_t`.
+     *
+     */
+    enum KeyIdMode : uint8_t
+    {
+        kKeyIdMode0 = 0 << 3, ///< Key ID Mode 0 - Key is determined implicitly.
+        kKeyIdMode1 = 1 << 3, ///< Key ID Mode 1 - Key is determined from Key Index field.
+        kKeyIdMode2 = 2 << 3, ///< Key ID Mode 2 - Key is determined from 4-bytes Key Source and Index fields.
+        kKeyIdMode3 = 3 << 3, ///< Key ID Mode 3 - Key is determined from 8-bytes Key Source and Index fields.
+    };
 
-    static constexpr uint8_t kKeyIdMode0    = 0 << 3;
-    static constexpr uint8_t kKeyIdMode1    = 1 << 3;
-    static constexpr uint8_t kKeyIdMode2    = 2 << 3;
-    static constexpr uint8_t kKeyIdMode3    = 3 << 3;
-    static constexpr uint8_t kKeyIdModeMask = 3 << 3;
-
-    static constexpr uint8_t kKeySourceSizeMode0 = 0;
-    static constexpr uint8_t kKeySourceSizeMode1 = 0;
-    static constexpr uint8_t kKeySourceSizeMode2 = 4;
-    static constexpr uint8_t kKeySourceSizeMode3 = 8;
-
-    static constexpr uint8_t kKeyIndexSize = sizeof(uint8_t);
-
-    static constexpr uint8_t kMacCmdAssociationRequest         = 1;
-    static constexpr uint8_t kMacCmdAssociationResponse        = 2;
-    static constexpr uint8_t kMacCmdDisassociationNotification = 3;
-    static constexpr uint8_t kMacCmdDataRequest                = 4;
-    static constexpr uint8_t kMacCmdPanidConflictNotification  = 5;
-    static constexpr uint8_t kMacCmdOrphanNotification         = 6;
-    static constexpr uint8_t kMacCmdBeaconRequest              = 7;
-    static constexpr uint8_t kMacCmdCoordinatorRealignment     = 8;
-    static constexpr uint8_t kMacCmdGtsRequest                 = 9;
-
-    static constexpr uint8_t kImmAckLength = kFcfSize + kDsnSize + k154FcsSize;
+    /**
+     * This enumeration represents a subset of MAC Command Identifiers.
+     *
+     */
+    enum CommandId : uint8_t
+    {
+        kMacCmdAssociationRequest         = 1,
+        kMacCmdAssociationResponse        = 2,
+        kMacCmdDisassociationNotification = 3,
+        kMacCmdDataRequest                = 4,
+        kMacCmdPanidConflictNotification  = 5,
+        kMacCmdOrphanNotification         = 6,
+        kMacCmdBeaconRequest              = 7,
+        kMacCmdCoordinatorRealignment     = 8,
+        kMacCmdGtsRequest                 = 9,
+    };
 
     static constexpr uint16_t kInfoStringSize = 128; ///< Max chars for `InfoString` (ToInfoString()).
 
@@ -365,11 +371,26 @@
     /**
      * This method initializes the MAC header.
      *
-     * @param[in]  aFcf              The Frame Control field.
-     * @param[in]  aSecurityControl  The Security Control field.
+     * This method determines and writes the Frame Control Field (FCF) and Security Control in the frame along with
+     * given source and destination addresses and PAN IDs.
+     *
+     * The Ack Request bit in FCF is set if there is destination and it is not broadcast. The Frame Pending and IE
+     * Present bits are not set.
+     *
+     * @param[in] aType          Frame type.
+     * @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] aSecurityLevel Frame security level.
+     * @param[in] aKeyIdMode     Frame security key ID mode.
      *
      */
-    void InitMacHeader(uint16_t aFcf, uint8_t aSecurityControl);
+    void InitMacHeader(Type             aType,
+                       Version          aVersion,
+                       const Addresses &aAddrs,
+                       const PanIds    &aPanIds,
+                       SecurityLevel    aSecurityLevel,
+                       KeyIdMode        aKeyIdMode = kKeyIdMode0);
 
     /**
      * This method validates the frame.
@@ -395,7 +416,7 @@
      * @retval FALSE  If this is not an Ack.
      *
      */
-    bool IsAck(void) const { return GetType() == kFcfFrameAck; }
+    bool IsAck(void) const { return GetType() == kTypeAck; }
 
     /**
      * This method returns the IEEE 802.15.4 Frame Version.
@@ -816,14 +837,6 @@
     uint8_t GetChannel(void) const { return mChannel; }
 
     /**
-     * This method sets the IEEE 802.15.4 channel used for transmission or reception.
-     *
-     * @param[in]  aChannel  The IEEE 802.15.4 channel used for transmission or reception.
-     *
-     */
-    void SetChannel(uint8_t aChannel) { mChannel = aChannel; }
-
-    /**
      * This method returns the IEEE 802.15.4 PSDU length.
      *
      * @returns The IEEE 802.15.4 PSDU length.
@@ -918,6 +931,8 @@
     /**
      * This template method appends an Header IE at specified index in this frame.
      *
+     * This method also sets the IE present bit in the Frame Control Field (FCF).
+     *
      * @param[in,out]   aIndex  The index to append IE. If `aIndex` is `0` on input, this method finds the index
      *                          for the first IE and appends the IE at that position. If the position is not found
      *                          successfully, `aIndex` will be set to `kInvalidIndex`. Otherwise the IE will be
@@ -1066,6 +1081,46 @@
     uint16_t GetFrameControlField(void) const;
 
 protected:
+    static constexpr uint8_t kFcfSize             = sizeof(uint16_t);
+    static constexpr uint8_t kDsnSize             = sizeof(uint8_t);
+    static constexpr uint8_t kSecurityControlSize = sizeof(uint8_t);
+    static constexpr uint8_t kFrameCounterSize    = sizeof(uint32_t);
+    static constexpr uint8_t kCommandIdSize       = sizeof(uint8_t);
+    static constexpr uint8_t k154FcsSize          = sizeof(uint16_t);
+    static constexpr uint8_t kKeyIndexSize        = sizeof(uint8_t);
+
+    static constexpr uint16_t kFcfFrameTypeMask    = 7 << 0;
+    static constexpr uint16_t kFcfSecurityEnabled  = 1 << 3;
+    static constexpr uint16_t kFcfFramePending     = 1 << 4;
+    static constexpr uint16_t kFcfAckRequest       = 1 << 5;
+    static constexpr uint16_t kFcfPanidCompression = 1 << 6;
+    static constexpr uint16_t kFcfIePresent        = 1 << 9;
+    static constexpr uint16_t kFcfDstAddrNone      = 0 << 10;
+    static constexpr uint16_t kFcfDstAddrShort     = 2 << 10;
+    static constexpr uint16_t kFcfDstAddrExt       = 3 << 10;
+    static constexpr uint16_t kFcfDstAddrMask      = 3 << 10;
+    static constexpr uint16_t kFcfFrameVersionMask = 3 << 12;
+    static constexpr uint16_t kFcfSrcAddrNone      = 0 << 14;
+    static constexpr uint16_t kFcfSrcAddrShort     = 2 << 14;
+    static constexpr uint16_t kFcfSrcAddrExt       = 3 << 14;
+    static constexpr uint16_t kFcfSrcAddrMask      = 3 << 14;
+
+    static constexpr uint8_t kSecLevelMask  = 7 << 0;
+    static constexpr uint8_t kKeyIdModeMask = 3 << 3;
+
+    static constexpr uint8_t kMic0Size   = 0;
+    static constexpr uint8_t kMic32Size  = 32 / CHAR_BIT;
+    static constexpr uint8_t kMic64Size  = 64 / CHAR_BIT;
+    static constexpr uint8_t kMic128Size = 128 / CHAR_BIT;
+    static constexpr uint8_t kMaxMicSize = kMic128Size;
+
+    static constexpr uint8_t kKeySourceSizeMode0 = 0;
+    static constexpr uint8_t kKeySourceSizeMode1 = 0;
+    static constexpr uint8_t kKeySourceSizeMode2 = 4;
+    static constexpr uint8_t kKeySourceSizeMode3 = 8;
+
+    static constexpr uint8_t kImmAckLength = kFcfSize + kDsnSize + k154FcsSize;
+
     static constexpr uint8_t kInvalidIndex  = 0xff;
     static constexpr uint8_t kInvalidSize   = kInvalidIndex;
     static constexpr uint8_t kMaxPsduSize   = kInvalidSize - 1;
@@ -1092,7 +1147,7 @@
     static bool IsDstPanIdPresent(uint16_t aFcf);
     static bool IsSrcAddrPresent(uint16_t aFcf) { return (aFcf & kFcfSrcAddrMask) != kFcfSrcAddrNone; }
     static bool IsSrcPanIdPresent(uint16_t aFcf);
-    static bool IsVersion2015(uint16_t aFcf) { return (aFcf & kFcfFrameVersionMask) == kFcfFrameVersion2015; }
+    static bool IsVersion2015(uint16_t aFcf) { return (aFcf & kFcfFrameVersionMask) == kVersion2015; }
 
     static uint8_t CalculateAddrFieldSize(uint16_t aFcf);
     static uint8_t CalculateSecurityHeaderSize(uint8_t aSecurityControl);
@@ -1151,8 +1206,9 @@
 
     /**
      * This method returns the timestamp when the frame was received.
+     * The timestamp marks the frame detection time: the end of the last symbol of SFD.
      *
-     * @returns The timestamp when the frame was received, in microseconds.
+     * @returns The timestamp when the frame SFD was received, in microseconds.
      *
      */
     const uint64_t &GetTimestamp(void) const { return mInfo.mRxInfo.mTimestamp; }
@@ -1200,6 +1256,36 @@
 {
 public:
     /**
+     * This method sets the channel on which to send the frame.
+     *
+     * It also sets the `RxChannelAfterTxDone` to the same channel.
+     *
+     * @param[in]  aChannel  The channel used for transmission.
+     *
+     */
+    void SetChannel(uint8_t aChannel)
+    {
+        mChannel = aChannel;
+        SetRxChannelAfterTxDone(aChannel);
+    }
+
+    /**
+     * This method gets the RX channel after frame TX is done.
+     *
+     * @returns The RX channel after frame TX is done.
+     *
+     */
+    uint8_t GetRxChannelAfterTxDone(void) const { return mInfo.mTxInfo.mRxChannelAfterTxDone; }
+
+    /**
+     * This method sets the RX channel after frame TX is done.
+     *
+     * @param[in] aChannel   The RX channel after frame TX is done.
+     *
+     */
+    void SetRxChannelAfterTxDone(uint8_t aChannel) { mInfo.mTxInfo.mRxChannelAfterTxDone = aChannel; }
+
+    /**
      * This method returns the maximum number of backoffs the CSMA-CA algorithm will attempt before declaring a channel
      * access failure.
      *
diff --git a/src/core/mac/mac_links.hpp b/src/core/mac/mac_links.hpp
index 99406f7..cc30ef0 100644
--- a/src/core/mac/mac_links.hpp
+++ b/src/core/mac/mac_links.hpp
@@ -41,6 +41,7 @@
 #include "mac/mac_frame.hpp"
 #include "mac/mac_types.hpp"
 #include "mac/sub_mac.hpp"
+#include "radio/radio.hpp"
 #include "radio/trel_link.hpp"
 
 namespace ot {
@@ -291,8 +292,6 @@
     friend class ot::Instance;
 
 public:
-    static const int8_t kInvalidRssiValue = SubMac::kInvalidRssiValue; ///< Invalid RSSI value.
-
     /**
      * This constructor initializes the `Links` object.
      *
@@ -576,7 +575,7 @@
     /**
      * This method gets the most recent RSSI measurement from radio link.
      *
-     * @returns The RSSI in dBm when it is valid. `kInvalidRssiValue` when RSSI is invalid.
+     * @returns The RSSI in dBm when it is valid. `Radio::kInvalidRssi` when RSSI is invalid.
      *
      */
     int8_t GetRssi(void) const
@@ -585,7 +584,7 @@
 #if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
             mSubMac.GetRssi();
 #else
-            kInvalidRssiValue;
+            Radio::kInvalidRssi;
 #endif
     }
 
@@ -620,7 +619,7 @@
      * @returns The noise floor value in dBm.
      *
      */
-    int8_t GetNoiseFloor(void)
+    int8_t GetNoiseFloor(void) const
     {
         return
 #if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
@@ -682,7 +681,7 @@
 #endif
 
 private:
-    static constexpr int8_t kDefaultNoiseFloor = -100;
+    static constexpr int8_t kDefaultNoiseFloor = Radio::kDefaultReceiveSensitivity;
 
     SubMac mSubMac;
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
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 483a5b9..6a73244 100644
--- a/src/core/mac/mac_types.hpp
+++ b/src/core/mac/mac_types.hpp
@@ -412,6 +412,26 @@
 };
 
 /**
+ * This structure represents two MAC addresses corresponding to source and destination.
+ *
+ */
+struct Addresses
+{
+    Address mSource;      ///< Source address.
+    Address mDestination; ///< Destination address.
+};
+
+/**
+ * This structure represents two PAN IDs corresponding to source and destination.
+ *
+ */
+struct PanIds
+{
+    PanId mSource;      ///< Source PAN ID.
+    PanId mDestination; ///< Destination PAN ID.
+};
+
+/**
  * This class represents a MAC key.
  *
  */
@@ -520,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`.
@@ -857,6 +877,73 @@
 };
 
 /**
+ * This class represents CSL accuracy.
+ *
+ */
+class CslAccuracy
+{
+public:
+    static constexpr uint8_t kWorstClockAccuracy = 255; ///< Worst possible crystal accuracy, in units of ± ppm.
+    static constexpr uint8_t kWorstUncertainty   = 255; ///< Worst possible uncertainty, in units of 10 microseconds.
+
+    /**
+     * This method initializes the CSL accuracy using `kWorstClockAccuracy` and `kWorstUncertainty` values.
+     *
+     */
+    void Init(void)
+    {
+        mClockAccuracy = kWorstClockAccuracy;
+        mUncertainty   = kWorstUncertainty;
+    }
+
+    /**
+     * This method returns the CSL clock accuracy.
+     *
+     * @returns The CSL clock accuracy in ± ppm.
+     *
+     */
+    uint8_t GetClockAccuracy(void) const { return mClockAccuracy; }
+
+    /**
+     * This method sets the CSL clock accuracy.
+     *
+     * @param[in]  aClockAccuracy  The CSL clock accuracy in ± ppm.
+     *
+     */
+    void SetClockAccuracy(uint8_t aClockAccuracy) { mClockAccuracy = aClockAccuracy; }
+
+    /**
+     * This method returns the CSL uncertainty.
+     *
+     * @returns The uncertainty in units 10 microseconds.
+     *
+     */
+    uint8_t GetUncertainty(void) const { return mUncertainty; }
+
+    /**
+     * This method gets the CLS uncertainty in microseconds.
+     *
+     * @returns the CLS uncertainty in microseconds.
+     *
+     */
+    uint16_t GetUncertaintyInMicrosec(void) const { return static_cast<uint16_t>(mUncertainty) * kUsPerUncertUnit; }
+
+    /**
+     * This method sets the CSL uncertainty.
+     *
+     * @param[in]  aUncertainty  The CSL uncertainty in units 10 microseconds.
+     *
+     */
+    void SetUncertainty(uint8_t aUncertainty) { mUncertainty = aUncertainty; }
+
+private:
+    static constexpr uint8_t kUsPerUncertUnit = 10;
+
+    uint8_t mClockAccuracy;
+    uint8_t mUncertainty;
+};
+
+/**
  * @}
  *
  */
diff --git a/src/core/mac/sub_mac.cpp b/src/core/mac/sub_mac.cpp
index 7d5cccd..7d7ac17 100644
--- a/src/core/mac/sub_mac.cpp
+++ b/src/core/mac/sub_mac.cpp
@@ -42,6 +42,7 @@
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 #include "common/time.hpp"
 #include "mac/mac_frame.hpp"
@@ -56,15 +57,15 @@
     , mRadioCaps(Get<Radio>().GetCaps())
     , mTransmitFrame(Get<Radio>().GetTransmitBuffer())
     , mCallbacks(aInstance)
-    , mPcapCallback(nullptr)
-    , mPcapCallbackContext(nullptr)
-    , mTimer(aInstance, SubMac::HandleTimer)
+    , mTimer(aInstance)
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    , mCslParentAccuracy(kCslWorstCrystalPpm)
-    , mCslParentUncert(kCslWorstUncertainty)
     , mCslTimer(aInstance, SubMac::HandleCslTimer)
 #endif
 {
+#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+    mCslParentAccuracy.Init();
+#endif
+
     Init();
 }
 
@@ -76,7 +77,7 @@
     mShortAddress    = kShortAddrInvalid;
     mExtAddress.Clear();
     mRxOnWhenBackoff   = true;
-    mEnergyScanMaxRssi = kInvalidRssiValue;
+    mEnergyScanMaxRssi = Radio::kInvalidRssi;
     mEnergyScanEndTime = Time{0};
 #if OPENTHREAD_CONFIG_MAC_ADD_DELAY_ON_NO_ACK_ERROR_BEFORE_RETRY
     mRetxDelayBackOffExponent = kRetxDelayMinBackoffExponent;
@@ -175,12 +176,6 @@
     LogDebg("RadioExtAddress: %s", mExtAddress.ToString().AsCString());
 }
 
-void SubMac::SetPcapCallback(otLinkPcapCallback aPcapCallback, void *aCallbackContext)
-{
-    mPcapCallback        = aPcapCallback;
-    mPcapCallbackContext = aCallbackContext;
-}
-
 Error SubMac::Enable(void)
 {
     Error error = kErrorNone;
@@ -283,14 +278,14 @@
 
 void SubMac::HandleReceiveDone(RxFrame *aFrame, Error aError)
 {
-    if (mPcapCallback && (aFrame != nullptr) && (aError == kErrorNone))
+    if (mPcapCallback.IsSet() && (aFrame != nullptr) && (aError == kErrorNone))
     {
-        mPcapCallback(aFrame, false, mPcapCallbackContext);
+        mPcapCallback.Invoke(aFrame, false);
     }
 
     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
@@ -304,10 +299,11 @@
 
 #if OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE
         // Split the log into two lines for RTT to output
-        LogDebg("Received frame in state (SubMac %s, CSL %s), timestamp %u", StateToString(mState),
-                mIsCslSampling ? "CslSample" : "CslSleep", static_cast<uint32_t>(aFrame->mInfo.mRxInfo.mTimestamp));
-        LogDebg("Target sample start time %u, time drift %d", mCslSampleTime.GetValue(),
-                static_cast<uint32_t>(aFrame->mInfo.mRxInfo.mTimestamp) - mCslSampleTime.GetValue());
+        LogDebg("Received frame in state (SubMac %s, CSL %s), timestamp %lu", StateToString(mState),
+                mIsCslSampling ? "CslSample" : "CslSleep",
+                ToUlong(static_cast<uint32_t>(aFrame->mInfo.mRxInfo.mTimestamp)));
+        LogDebg("Target sample start time %lu, time drift %lu", ToUlong(mCslSampleTime.GetValue()),
+                ToUlong(static_cast<uint32_t>(aFrame->mInfo.mRxInfo.mTimestamp) - mCslSampleTime.GetValue()));
 #endif
     }
 #endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
@@ -335,16 +331,15 @@
 #if OPENTHREAD_CONFIG_MAC_ADD_DELAY_ON_NO_ACK_ERROR_BEFORE_RETRY
     case kStateDelayBeforeRetx:
 #endif
-    case kStateEnergyScan:
-        ExitNow(error = kErrorInvalidState);
-        OT_UNREACHABLE_CODE(break);
-
     case kStateSleep:
     case kStateReceive:
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
     case kStateCslSample:
 #endif
         break;
+
+    case kStateEnergyScan:
+        ExitNow(error = kErrorInvalidState);
     }
 
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
@@ -395,7 +390,7 @@
         uint32_t frameCounter = GetFrameCounter();
 
         mTransmitFrame.SetFrameCounter(frameCounter);
-        SignalFrameCounterUsed(frameCounter);
+        SignalFrameCounterUsed(frameCounter, mKeyId);
     }
 
     extAddress = &GetExtAddress();
@@ -424,9 +419,10 @@
         {
             if (Time(static_cast<uint32_t>(otPlatRadioGetNow(&GetInstance()))) <
                 Time(mTransmitFrame.mInfo.mTxInfo.mTxDelayBaseTime) + mTransmitFrame.mInfo.mTxInfo.mTxDelay -
-                    kCcaSampleInterval)
+                    kCcaSampleInterval - kCslTransmitTimeAhead)
             {
-                mTimer.StartAt(Time(mTransmitFrame.mInfo.mTxInfo.mTxDelayBaseTime) - kCcaSampleInterval,
+                mTimer.StartAt(Time(mTransmitFrame.mInfo.mTxInfo.mTxDelayBaseTime) - kCcaSampleInterval -
+                                   kCslTransmitTimeAhead,
                                mTransmitFrame.mInfo.mTxInfo.mTxDelay);
             }
             else // Transmit without delay
@@ -483,7 +479,7 @@
 #if OPENTHREAD_CONFIG_MAC_ADD_DELAY_ON_NO_ACK_ERROR_BEFORE_RETRY
     if (mState == kStateDelayBeforeRetx)
     {
-        LogDebg("Delaying retx for %u usec (be=%d)", backoff, aBackoffExponent);
+        LogDebg("Delaying retx for %lu usec (be=%u)", ToUlong(backoff), aBackoffExponent);
     }
 #endif
 }
@@ -505,9 +501,9 @@
 
     SetState(kStateTransmit);
 
-    if (mPcapCallback)
+    if (mPcapCallback.IsSet())
     {
-        mPcapCallback(&mTransmitFrame, true, mPcapCallbackContext);
+        mPcapCallback.Invoke(&mTransmitFrame, true);
     }
 
     error = Get<Radio>().Transmit(mTransmitFrame);
@@ -612,7 +608,8 @@
         {
             SetState(kStateDelayBeforeRetx);
             StartTimerForBackoff(mRetxDelayBackOffExponent);
-            mRetxDelayBackOffExponent = OT_MIN(mRetxDelayBackOffExponent + 1, kRetxDelayMaxBackoffExponent);
+            mRetxDelayBackOffExponent =
+                Min(static_cast<uint8_t>(mRetxDelayBackOffExponent + 1), kRetxDelayMaxBackoffExponent);
             ExitNow();
         }
 #endif
@@ -623,6 +620,19 @@
 
     SetState(kStateReceive);
 
+#if OPENTHREAD_RADIO
+    if (aFrame.GetChannel() != aFrame.GetRxChannelAfterTxDone())
+    {
+        // On RCP build, we switch immediately to the specified RX
+        // channel if it is different from the channel on which frame
+        // was sent. On FTD or MTD builds we don't need to do
+        // the same as the `Mac` will switch the channel from the
+        // `mCallbacks.TransmitDone()`.
+
+        IgnoreError(Get<Radio>().Receive(aFrame.GetRxChannelAfterTxDone()));
+    }
+#endif
+
     mCallbacks.TransmitDone(aFrame, aAckFrame, aError);
 
 exit:
@@ -632,6 +642,7 @@
 void SubMac::SignalFrameCounterUsedOnTxDone(const TxFrame &aFrame)
 {
     uint8_t  keyIdMode;
+    uint8_t  keyId;
     uint32_t frameCounter;
     bool     allowError = false;
 
@@ -656,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;
@@ -669,7 +682,7 @@
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
     if (mRadioFilterEnabled)
     {
-        rssi = kInvalidRssiValue;
+        rssi = Radio::kInvalidRssi;
     }
     else
 #endif
@@ -680,10 +693,7 @@
     return rssi;
 }
 
-int8_t SubMac::GetNoiseFloor(void)
-{
-    return Get<Radio>().GetReceiveSensitivity();
-}
+int8_t SubMac::GetNoiseFloor(void) const { return Get<Radio>().GetReceiveSensitivity(); }
 
 Error SubMac::EnergyScan(uint8_t aScanChannel, uint16_t aScanDuration)
 {
@@ -712,7 +722,7 @@
     }
 
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
-    VerifyOrExit(!mRadioFilterEnabled, HandleEnergyScanDone(kInvalidRssiValue));
+    VerifyOrExit(!mRadioFilterEnabled, HandleEnergyScanDone(Radio::kInvalidRssi));
 #endif
 
     if (RadioSupportsEnergyScan())
@@ -725,7 +735,7 @@
         SuccessOrAssert(Get<Radio>().Receive(aScanChannel));
 
         SetState(kStateEnergyScan);
-        mEnergyScanMaxRssi = kInvalidRssiValue;
+        mEnergyScanMaxRssi = Radio::kInvalidRssi;
         mEnergyScanEndTime = TimerMilli::GetNow() + static_cast<uint32_t>(aScanDuration);
         mTimer.Start(0);
     }
@@ -744,9 +754,9 @@
 
     int8_t rssi = GetRssi();
 
-    if (rssi != kInvalidRssiValue)
+    if (rssi != Radio::kInvalidRssi)
     {
-        if ((mEnergyScanMaxRssi == kInvalidRssiValue) || (rssi > mEnergyScanMaxRssi))
+        if ((mEnergyScanMaxRssi == Radio::kInvalidRssi) || (rssi > mEnergyScanMaxRssi))
         {
             mEnergyScanMaxRssi = rssi;
         }
@@ -772,11 +782,6 @@
     mCallbacks.EnergyScanDone(aMaxRssi);
 }
 
-void SubMac::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<SubMac>().HandleTimer();
-}
-
 void SubMac::HandleTimer(void)
 {
     switch (mState)
@@ -959,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
@@ -978,13 +985,23 @@
     return;
 }
 
-void SubMac::SetFrameCounter(uint32_t aFrameCounter)
+void SubMac::SetFrameCounter(uint32_t aFrameCounter, bool aSetIfLarger)
 {
-    mFrameCounter = aFrameCounter;
+    if (!aSetIfLarger || (aFrameCounter > mFrameCounter))
+    {
+        mFrameCounter = aFrameCounter;
+    }
 
     VerifyOrExit(!ShouldHandleTransmitSecurity());
 
-    Get<Radio>().SetMacFrameCounter(aFrameCounter);
+    if (aSetIfLarger)
+    {
+        Get<Radio>().SetMacFrameCounterIfLarger(aFrameCounter);
+    }
+    else
+    {
+        Get<Radio>().SetMacFrameCounter(aFrameCounter);
+    }
 
 exit:
     return;
@@ -1075,10 +1092,7 @@
     return retval;
 }
 
-void SubMac::HandleCslTimer(Timer &aTimer)
-{
-    aTimer.Get<SubMac>().HandleCslTimer();
-}
+void SubMac::HandleCslTimer(Timer &aTimer) { aTimer.Get<SubMac>().HandleCslTimer(); }
 
 void SubMac::HandleCslTimer(void)
 {
@@ -1090,6 +1104,32 @@
      *    |           |            |           |           |            |            |                        |
      * ---|-----------|------------|-----------|-----------|------------|------------|----------//------------|---
      * -timeAhead                           CslPhase                             +timeAfter             -timeAhead
+     *
+     * The handler works in different ways when the radio supports receive-timing and doesn't.
+     *
+     * When the radio supports receive-timing:
+     *   The handler will be called once per CSL period. When the handler is called, it will set the timer to
+     *   fire at the next CSL sample time and call `Radio::ReceiveAt` to start sampling for the current CSL period.
+     *   The timer fires some time before the actual sample time. After `Radio::ReceiveAt` is called, the radio will
+     *   remain in sleep state until the actual sample time.
+     *   Note that it never call `Radio::Sleep` explicitly. The radio will fall into sleep after `ReceiveAt` ends. This
+     *   will be done by the platform as part of the `otPlatRadioReceiveAt` API.
+     *
+     *   Timer fires                                         Timer fires
+     *       ^                                                    ^
+     *       x-|------------|-------------------------------------x-|------------|---------------------------------------|
+     *            sample                   sleep                        sample                    sleep
+     *
+     * When the radio doesn't support receive-timing:
+     *   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`.
+     *
+     *   Timer fires  Timer fires                            Timer fires  Timer fires
+     *       ^            ^                                       ^            ^
+     *       |------------|---------------------------------------|------------|---------------------------------------|
+     *          sample                   sleep                        sample                    sleep
+     *
      */
     uint32_t periodUs = mCslPeriod * kUsPerTenSymbols;
     uint32_t timeAhead, timeAfter;
@@ -1105,7 +1145,7 @@
 #if !OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE
             IgnoreError(Get<Radio>().Sleep()); // Don't actually sleep for debugging
 #endif
-            LogDebg("CSL sleep %u", mCslTimer.GetNow().GetValue());
+            LogDebg("CSL sleep %lu", ToUlong(mCslTimer.GetNow().GetValue()));
         }
     }
     else
@@ -1125,7 +1165,9 @@
 
         Get<Radio>().UpdateCslSampleTime(mCslSampleTime.GetValue());
 
-        if (RadioSupportsReceiveTiming() && (mState != kStateDisabled))
+        // 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))
         {
             IgnoreError(Get<Radio>().ReceiveAt(mCslChannel, mCslSampleTime.GetValue() - periodUs - timeAhead,
                                                timeAhead + timeAfter));
@@ -1133,7 +1175,8 @@
         else if (mState == kStateCslSample)
         {
             IgnoreError(Get<Radio>().Receive(mCslChannel));
-            LogDebg("CSL sample %u, duration %u", mCslTimer.GetNow().GetValue(), timeAhead + timeAfter);
+            LogDebg("CSL sample %lu, duration %lu", ToUlong(mCslTimer.GetNow().GetValue()),
+                    ToUlong(timeAhead + timeAfter));
         }
     }
 }
@@ -1147,12 +1190,13 @@
 
     elapsed = curTime - mCslLastSync.GetValue();
 
-    semiWindow = static_cast<uint32_t>(static_cast<uint64_t>(elapsed) *
-                                       (Get<Radio>().GetCslAccuracy() + mCslParentAccuracy) / 1000000);
-    semiWindow += mCslParentUncert * kUsPerUncertUnit;
+    semiWindow =
+        static_cast<uint32_t>(static_cast<uint64_t>(elapsed) *
+                              (Get<Radio>().GetCslAccuracy() + mCslParentAccuracy.GetClockAccuracy()) / 1000000);
+    semiWindow += mCslParentAccuracy.GetUncertaintyInMicrosec() + Get<Radio>().GetCslUncertainty() * 10;
 
-    aAhead = (semiWindow + kCslReceiveTimeAhead > semiPeriod) ? semiPeriod : semiWindow + kCslReceiveTimeAhead;
-    aAfter = (semiWindow + kMinCslWindow > semiPeriod) ? semiPeriod : semiWindow + kMinCslWindow;
+    aAhead = Min(semiPeriod, semiWindow + kCslReceiveTimeAhead);
+    aAfter = Min(semiPeriod, semiWindow + kMinCslWindow);
 }
 #endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 
diff --git a/src/core/mac/sub_mac.hpp b/src/core/mac/sub_mac.hpp
index 8eccfe9..e563210 100644
--- a/src/core/mac/sub_mac.hpp
+++ b/src/core/mac/sub_mac.hpp
@@ -40,6 +40,7 @@
 
 #include <openthread/platform/crypto.h>
 
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "common/timer.hpp"
@@ -108,8 +109,6 @@
     friend class LinkRaw;
 
 public:
-    static constexpr int8_t kInvalidRssiValue = 127; ///< Invalid Received Signal Strength Indicator (RSSI) value.
-
     /**
      * This class defines the callbacks notifying `SubMac` user of changes and events.
      *
@@ -167,7 +166,7 @@
          *
          */
         void RecordFrameTransmitStatus(const TxFrame &aFrame,
-                                       const RxFrame *aAckFrame,
+                                       RxFrame       *aAckFrame,
                                        Error          aError,
                                        uint8_t        aRetryCount,
                                        bool           aWillRetx);
@@ -189,7 +188,7 @@
         /**
          * This method notifies user of `SubMac` that energy scan is complete.
          *
-         * @param[in]  aMaxRssi  Maximum RSSI seen on the channel, or `SubMac::kInvalidRssiValue` if failed.
+         * @param[in]  aMaxRssi  Maximum RSSI seen on the channel, or `Radio::kInvalidRssi` if failed.
          *
          */
         void EnergyScanDone(int8_t aMaxRssi);
@@ -278,7 +277,10 @@
      * @param[in]  aCallbackContext  A pointer to application-specific context.
      *
      */
-    void SetPcapCallback(otLinkPcapCallback aPcapCallback, void *aCallbackContext);
+    void SetPcapCallback(otLinkPcapCallback aPcapCallback, void *aCallbackContext)
+    {
+        mPcapCallback.Set(aPcapCallback, aCallbackContext);
+    }
 
     /**
      * This method indicates whether radio should stay in Receive or Sleep during CSMA backoff.
@@ -367,7 +369,7 @@
     /**
      * This method gets the most recent RSSI measurement.
      *
-     * @returns The RSSI in dBm when it is valid. `kInvalidRssiValue` when RSSI is invalid.
+     * @returns The RSSI in dBm when it is valid. `Radio::kInvalidRssi` when RSSI is invalid.
      *
      */
     int8_t GetRssi(void) const;
@@ -392,7 +394,7 @@
      * @returns The noise floor value in dBm.
      *
      */
-    int8_t GetNoiseFloor(void);
+    int8_t GetNoiseFloor(void) const;
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
     /**
@@ -418,36 +420,20 @@
     void CslSample(void);
 
     /**
-     * This method returns CSL parent clock accuracy, in ± ppm.
+     * This method returns parent CSL accuracy (clock accuracy and uncertainty).
      *
-     * @retval CSL parent clock accuracy.
+     * @returns The parent CSL accuracy.
      *
      */
-    uint8_t GetCslParentClockAccuracy(void) const { return mCslParentAccuracy; }
+    const CslAccuracy &GetCslParentAccuracy(void) const { return mCslParentAccuracy; }
 
     /**
-     * This method sets CSL parent clock accuracy, in ± ppm.
+     * This method sets parent CSL accuracy.
      *
-     * @param[in] aCslParentAccuracy CSL parent clock accuracy, in ± ppm.
+     * @param[in] aCslAccuracy  The parent CSL accuracy.
      *
      */
-    void SetCslParentClockAccuracy(uint8_t aCslParentAccuracy) { mCslParentAccuracy = aCslParentAccuracy; }
-
-    /**
-     * This method sets CSL parent uncertainty, in ±10 us units.
-     *
-     * @retval CSL parent uncertainty, in ±10 us units.
-     *
-     */
-    uint8_t GetCslParentUncertainty(void) const { return mCslParentUncert; }
-
-    /**
-     * This method returns CSL parent uncertainty, in ±10 us units.
-     *
-     * @param[in] aCslParentUncert  CSL parent uncertainty, in ±10 us units.
-     *
-     */
-    void SetCslParentUncertainty(uint8_t aCslParentUncert) { mCslParentUncert = aCslParentUncert; }
+    void SetCslParentAccuracy(const CslAccuracy &aCslAccuracy) { mCslParentAccuracy = aCslAccuracy; }
 
 #endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 
@@ -492,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.
@@ -503,9 +500,11 @@
      * This method sets the current MAC Frame Counter value.
      *
      * @param[in] aFrameCounter  The MAC Frame Counter value.
+     * @param[in] aSetIfLarger   If `true`, set only if the new value @p aFrameCounter is larger than the current value.
+     *                           If `false`, set the new value independent of the current value.
      *
      */
-    void SetFrameCounter(uint32_t aFrameCounter);
+    void SetFrameCounter(uint32_t aFrameCounter, bool aSetIfLarger);
 
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
     /**
@@ -583,6 +582,13 @@
     static constexpr uint32_t kCslReceiveTimeAhead = OPENTHREAD_CONFIG_CSL_RECEIVE_TIME_AHEAD;
 #endif
 
+#if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
+    // CSL transmitter would schedule delayed transmission `kCslTransmitTimeAhead` earlier
+    // than expected delayed transmit time. The value is in usec.
+    // Only for radios not supporting OT_RADIO_CAPS_TRANSMIT_TIMING.
+    static constexpr uint32_t kCslTransmitTimeAhead = OPENTHREAD_CONFIG_CSL_TRANSMIT_TIME_AHEAD;
+#endif
+
     /**
      * This method initializes the states of the sub-MAC layer.
      *
@@ -609,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);
@@ -620,13 +626,18 @@
     void HandleTransmitDone(TxFrame &aFrame, RxFrame *aAckFrame, Error aError);
     void SignalFrameCounterUsedOnTxDone(const TxFrame &aFrame);
     void HandleEnergyScanDone(int8_t aMaxRssi);
-
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTimer(void);
 
     void               SetState(State aState);
     static const char *StateToString(State aState);
 
+    using SubMacTimer =
+#if OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
+        TimerMicroIn<SubMac, &SubMac::HandleTimer>;
+#else
+        TimerMilliIn<SubMac, &SubMac::HandleTimer>;
+#endif
+
     otRadioCaps  mRadioCaps;
     State        mState;
     uint8_t      mCsmaBackoffs;
@@ -637,37 +648,31 @@
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
     bool mRadioFilterEnabled : 1;
 #endif
-    int8_t             mEnergyScanMaxRssi;
-    TimeMilli          mEnergyScanEndTime;
-    TxFrame &          mTransmitFrame;
-    Callbacks          mCallbacks;
-    otLinkPcapCallback mPcapCallback;
-    void *             mPcapCallbackContext;
-    KeyMaterial        mPrevKey;
-    KeyMaterial        mCurrKey;
-    KeyMaterial        mNextKey;
-    uint32_t           mFrameCounter;
-    uint8_t            mKeyId;
+    int8_t                       mEnergyScanMaxRssi;
+    TimeMilli                    mEnergyScanEndTime;
+    TxFrame                     &mTransmitFrame;
+    Callbacks                    mCallbacks;
+    Callback<otLinkPcapCallback> mPcapCallback;
+    KeyMaterial                  mPrevKey;
+    KeyMaterial                  mCurrKey;
+    KeyMaterial                  mNextKey;
+    uint32_t                     mFrameCounter;
+    uint8_t                      mKeyId;
 #if OPENTHREAD_CONFIG_MAC_ADD_DELAY_ON_NO_ACK_ERROR_BEFORE_RETRY
     uint8_t mRetxDelayBackOffExponent;
 #endif
-#if OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
-    TimerMicro mTimer;
-#else
-    TimerMilli                mTimer;
-#endif
+    SubMacTimer mTimer;
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
     uint16_t mCslPeriod;      // The CSL sample period, in units of 10 symbols (160 microseconds).
     uint8_t  mCslChannel : 7; // The CSL sample channel.
     bool mIsCslSampling : 1;  // Indicates that the radio is receiving in CSL state for platforms not supporting delayed
                               // reception.
-    uint16_t   mCslPeerShort; // The CSL peer short address.
-    TimeMicro  mCslSampleTime;     // The CSL sample time of the current period.
-    TimeMicro  mCslLastSync;       // The timestamp of the last successful CSL synchronization.
-    uint8_t    mCslParentAccuracy; // Drift of timer used for scheduling CSL tx by the parent, in ± ppm.
-    uint8_t    mCslParentUncert;   // Uncertainty of the scheduling CSL of tx by the parent, in ±10 us units.
-    TimerMicro mCslTimer;
+    uint16_t    mCslPeerShort;      // The CSL peer short address.
+    TimeMicro   mCslSampleTime;     // The CSL sample time of the current period.
+    TimeMicro   mCslLastSync;       // The timestamp of the last successful CSL synchronization.
+    CslAccuracy mCslParentAccuracy; // The parent's CSL accuracy (clock accuracy and uncertainty).
+    TimerMicro  mCslTimer;
 #endif
 };
 
diff --git a/src/core/mac/sub_mac_callbacks.cpp b/src/core/mac/sub_mac_callbacks.cpp
index b61ad28..4a3053d 100644
--- a/src/core/mac/sub_mac_callbacks.cpp
+++ b/src/core/mac/sub_mac_callbacks.cpp
@@ -71,7 +71,7 @@
 }
 
 void SubMac::Callbacks::RecordFrameTransmitStatus(const TxFrame &aFrame,
-                                                  const RxFrame *aAckFrame,
+                                                  RxFrame       *aAckFrame,
                                                   Error          aError,
                                                   uint8_t        aRetryCount,
                                                   bool           aWillRetx)
@@ -114,17 +114,12 @@
 
 #elif OPENTHREAD_RADIO
 
-void SubMac::Callbacks::ReceiveDone(RxFrame *aFrame, Error aError)
-{
-    Get<LinkRaw>().InvokeReceiveDone(aFrame, aError);
-}
+void SubMac::Callbacks::ReceiveDone(RxFrame *aFrame, Error aError) { Get<LinkRaw>().InvokeReceiveDone(aFrame, aError); }
 
-void SubMac::Callbacks::RecordCcaStatus(bool, uint8_t)
-{
-}
+void SubMac::Callbacks::RecordCcaStatus(bool, uint8_t) {}
 
 void SubMac::Callbacks::RecordFrameTransmitStatus(const TxFrame &aFrame,
-                                                  const RxFrame *aAckFrame,
+                                                  RxFrame       *aAckFrame,
                                                   Error          aError,
                                                   uint8_t        aRetryCount,
                                                   bool           aWillRetx)
@@ -137,15 +132,9 @@
     Get<LinkRaw>().InvokeTransmitDone(aFrame, aAckFrame, aError);
 }
 
-void SubMac::Callbacks::EnergyScanDone(int8_t aMaxRssi)
-{
-    Get<LinkRaw>().InvokeEnergyScanDone(aMaxRssi);
-}
+void SubMac::Callbacks::EnergyScanDone(int8_t aMaxRssi) { Get<LinkRaw>().InvokeEnergyScanDone(aMaxRssi); }
 
-void SubMac::Callbacks::FrameCounterUsed(uint32_t aFrameCounter)
-{
-    OT_UNUSED_VARIABLE(aFrameCounter);
-}
+void SubMac::Callbacks::FrameCounterUsed(uint32_t aFrameCounter) { OT_UNUSED_VARIABLE(aFrameCounter); }
 
 #endif // OPENTHREAD_RADIO
 
diff --git a/src/core/meshcop/announce_begin_client.cpp b/src/core/meshcop/announce_begin_client.cpp
index b1bb699..f0cde45 100644
--- a/src/core/meshcop/announce_begin_client.cpp
+++ b/src/core/meshcop/announce_begin_client.cpp
@@ -63,12 +63,12 @@
     Error                   error = kErrorNone;
     MeshCoP::ChannelMaskTlv channelMask;
     Tmf::MessageInfo        messageInfo(GetInstance());
-    Coap::Message *         message = nullptr;
+    Coap::Message          *message = nullptr;
 
     VerifyOrExit(Get<MeshCoP::Commissioner>().IsActive(), error = kErrorInvalidState);
     VerifyOrExit((message = Get<Tmf::Agent>().NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
 
-    SuccessOrExit(error = message->InitAsPost(aAddress, UriPath::kAnnounceBegin));
+    SuccessOrExit(error = message->InitAsPost(aAddress, kUriAnnounceBegin));
     SuccessOrExit(error = message->SetPayloadMarker());
 
     SuccessOrExit(
@@ -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 e10a79d..9081751 100644
--- a/src/core/meshcop/border_agent.cpp
+++ b/src/core/meshcop/border_agent.cpp
@@ -56,7 +56,7 @@
 constexpr uint16_t kBorderAgentUdpPort = OPENTHREAD_CONFIG_BORDER_AGENT_UDP_PORT; ///< UDP port of border agent service.
 }
 
-void BorderAgent::ForwardContext::Init(Instance &           aInstance,
+void BorderAgent::ForwardContext::Init(Instance            &aInstance,
                                        const Coap::Message &aMessage,
                                        bool                 aPetition,
                                        bool                 aSeparate)
@@ -113,13 +113,12 @@
 
 void BorderAgent::SendErrorMessage(ForwardContext &aForwardContext, Error aError)
 {
-    Error             error   = kErrorNone;
-    Coap::CoapSecure &coaps   = Get<Coap::CoapSecure>();
-    Coap::Message *   message = nullptr;
+    Error          error   = kErrorNone;
+    Coap::Message *message = nullptr;
 
-    VerifyOrExit((message = coaps.NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
+    VerifyOrExit((message = Get<Tmf::SecureAgent>().NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = aForwardContext.ToHeader(*message, CoapCodeFromError(aError)));
-    SuccessOrExit(error = coaps.SendMessage(*message, coaps.GetMessageInfo()));
+    SuccessOrExit(error = Get<Tmf::SecureAgent>().SendMessage(*message, Get<Tmf::SecureAgent>().GetMessageInfo()));
 
 exit:
     FreeMessageOnError(message, error);
@@ -128,11 +127,10 @@
 
 void BorderAgent::SendErrorMessage(const Coap::Message &aRequest, bool aSeparate, Error aError)
 {
-    Error             error   = kErrorNone;
-    Coap::CoapSecure &coaps   = Get<Coap::CoapSecure>();
-    Coap::Message *   message = nullptr;
+    Error          error   = kErrorNone;
+    Coap::Message *message = nullptr;
 
-    VerifyOrExit((message = coaps.NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
+    VerifyOrExit((message = Get<Tmf::SecureAgent>().NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
 
     if (aRequest.IsNonConfirmable() || aSeparate)
     {
@@ -150,15 +148,15 @@
 
     SuccessOrExit(error = message->SetTokenFromMessage(aRequest));
 
-    SuccessOrExit(error = coaps.SendMessage(*message, coaps.GetMessageInfo()));
+    SuccessOrExit(error = Get<Tmf::SecureAgent>().SendMessage(*message, Get<Tmf::SecureAgent>().GetMessageInfo()));
 
 exit:
     FreeMessageOnError(message, error);
     LogError("send error CoAP message", error);
 }
 
-void BorderAgent::HandleCoapResponse(void *               aContext,
-                                     otMessage *          aMessage,
+void BorderAgent::HandleCoapResponse(void                *aContext,
+                                     otMessage           *aMessage,
                                      const otMessageInfo *aMessageInfo,
                                      Error                aResult)
 {
@@ -175,7 +173,7 @@
     Error          error;
 
     SuccessOrExit(error = aResult);
-    VerifyOrExit((message = Get<Coap::CoapSecure>().NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
+    VerifyOrExit((message = Get<Tmf::SecureAgent>().NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
 
     if (aForwardContext.IsPetition() && aResponse->GetCode() == Coap::kCodeChanged)
     {
@@ -213,7 +211,7 @@
     {
         FreeMessage(message);
 
-        LogWarn("Commissioner request[%hu] failed: %s", aForwardContext.GetMessageId(), ErrorToString(error));
+        LogWarn("Commissioner request[%u] failed: %s", aForwardContext.GetMessageId(), ErrorToString(error));
 
         SendErrorMessage(aForwardContext, error);
     }
@@ -221,83 +219,45 @@
     Heap::Free(&aForwardContext);
 }
 
-template <Coap::Resource BorderAgent::*aResource>
-void BorderAgent::HandleRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    IgnoreError(static_cast<BorderAgent *>(aContext)->ForwardToLeader(
-        AsCoapMessage(aMessage), AsCoreType(aMessageInfo),
-        (static_cast<BorderAgent *>(aContext)->*aResource).GetUriPath(), false, false));
-}
-
-template <>
-void BorderAgent::HandleRequest<&BorderAgent::mCommissionerPetition>(void *               aContext,
-                                                                     otMessage *          aMessage,
-                                                                     const otMessageInfo *aMessageInfo)
-{
-    IgnoreError(static_cast<BorderAgent *>(aContext)->ForwardToLeader(AsCoapMessage(aMessage), AsCoreType(aMessageInfo),
-                                                                      UriPath::kLeaderPetition, true, true));
-}
-
-template <>
-void BorderAgent::HandleRequest<&BorderAgent::mCommissionerKeepAlive>(void *               aContext,
-                                                                      otMessage *          aMessage,
-                                                                      const otMessageInfo *aMessageInfo)
-{
-    static_cast<BorderAgent *>(aContext)->HandleKeepAlive(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-template <>
-void BorderAgent::HandleRequest<&BorderAgent::mRelayTransmit>(void *               aContext,
-                                                              otMessage *          aMessage,
-                                                              const otMessageInfo *aMessageInfo)
-{
-    OT_UNUSED_VARIABLE(aMessageInfo);
-    static_cast<BorderAgent *>(aContext)->HandleRelayTransmit(AsCoapMessage(aMessage));
-}
-
-template <>
-void BorderAgent::HandleRequest<&BorderAgent::mRelayReceive>(void *               aContext,
-                                                             otMessage *          aMessage,
-                                                             const otMessageInfo *aMessageInfo)
-{
-    OT_UNUSED_VARIABLE(aMessageInfo);
-    static_cast<BorderAgent *>(aContext)->HandleRelayReceive(AsCoapMessage(aMessage));
-}
-
-template <>
-void BorderAgent::HandleRequest<&BorderAgent::mProxyTransmit>(void *               aContext,
-                                                              otMessage *          aMessage,
-                                                              const otMessageInfo *aMessageInfo)
-{
-    OT_UNUSED_VARIABLE(aMessageInfo);
-    static_cast<BorderAgent *>(aContext)->HandleProxyTransmit(AsCoapMessage(aMessage));
-}
-
 BorderAgent::BorderAgent(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mCommissionerPetition(UriPath::kCommissionerPetition,
-                            BorderAgent::HandleRequest<&BorderAgent::mCommissionerPetition>,
-                            this)
-    , mCommissionerKeepAlive(UriPath::kCommissionerKeepAlive,
-                             BorderAgent::HandleRequest<&BorderAgent::mCommissionerKeepAlive>,
-                             this)
-    , mRelayTransmit(UriPath::kRelayTx, BorderAgent::HandleRequest<&BorderAgent::mRelayTransmit>, this)
-    , mRelayReceive(UriPath::kRelayRx, BorderAgent::HandleRequest<&BorderAgent::mRelayReceive>, this)
-    , mCommissionerGet(UriPath::kCommissionerGet, BorderAgent::HandleRequest<&BorderAgent::mCommissionerGet>, this)
-    , mCommissionerSet(UriPath::kCommissionerSet, BorderAgent::HandleRequest<&BorderAgent::mCommissionerSet>, this)
-    , mActiveGet(UriPath::kActiveGet, BorderAgent::HandleRequest<&BorderAgent::mActiveGet>, this)
-    , mActiveSet(UriPath::kActiveSet, BorderAgent::HandleRequest<&BorderAgent::mActiveSet>, this)
-    , mPendingGet(UriPath::kPendingGet, BorderAgent::HandleRequest<&BorderAgent::mPendingGet>, this)
-    , mPendingSet(UriPath::kPendingSet, BorderAgent::HandleRequest<&BorderAgent::mPendingSet>, this)
-    , mProxyTransmit(UriPath::kProxyTx, BorderAgent::HandleRequest<&BorderAgent::mProxyTransmit>, this)
     , mUdpReceiver(BorderAgent::HandleUdpReceive, this)
-    , mTimer(aInstance, HandleTimeout)
+    , 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(uint8_t *aId, uint16_t &aLength)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(aLength >= sizeof(mId), error = kErrorInvalidArgs);
+    VerifyOrExit(!mIdInitialized, error = kErrorNone);
+
+    if (Get<Settings>().Read(mId) != kErrorNone)
+    {
+        Random::NonCrypto::FillBuffer(mId.GetId(), sizeof(mId));
+        SuccessOrExit(error = Get<Settings>().Save(mId));
+    }
+
+    mIdInitialized = true;
+
+exit:
+    if (error == kErrorNone)
+    {
+        memcpy(aId, mId.GetId(), sizeof(mId));
+        aLength = static_cast<uint16_t>(sizeof(mId));
+    }
+    return error;
+}
+#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+
 void BorderAgent::HandleNotifierEvents(Events aEvents)
 {
     VerifyOrExit(aEvents.ContainsAny(kEventThreadRoleChanged | kEventCommissionerStateChanged));
@@ -319,31 +279,38 @@
     return;
 }
 
-void BorderAgent::HandleProxyTransmit(const Coap::Message &aMessage)
+template <> void BorderAgent::HandleTmf<kUriProxyTx>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    Message *           message = nullptr;
-    Ip6::MessageInfo    messageInfo;
-    uint16_t            offset;
-    Error               error;
-    UdpEncapsulationTlv tlv;
+    OT_UNUSED_VARIABLE(aMessageInfo);
 
-    SuccessOrExit(error = Tlv::FindTlvOffset(aMessage, Tlv::kUdpEncapsulation, offset));
-    SuccessOrExit(error = aMessage.Read(offset, tlv));
+    Error                     error   = kErrorNone;
+    Message                  *message = nullptr;
+    Ip6::MessageInfo          messageInfo;
+    uint16_t                  offset;
+    uint16_t                  length;
+    UdpEncapsulationTlvHeader udpEncapHeader;
 
-    VerifyOrExit((message = Get<Ip6::Udp>().NewMessage(0)) != nullptr, error = kErrorNoBufs);
-    SuccessOrExit(error = message->SetLength(tlv.GetUdpLength()));
-    aMessage.CopyTo(offset + sizeof(tlv), 0, tlv.GetUdpLength(), *message);
+    VerifyOrExit(mState != kStateStopped);
 
-    VerifyOrExit(tlv.GetSourcePort() > 0 && tlv.GetDestinationPort() > 0, error = kErrorDrop);
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Tlv::kUdpEncapsulation, offset, length));
 
-    messageInfo.SetSockPort(tlv.GetSourcePort());
+    SuccessOrExit(error = aMessage.Read(offset, udpEncapHeader));
+    offset += sizeof(UdpEncapsulationTlvHeader);
+    length -= sizeof(UdpEncapsulationTlvHeader);
+
+    VerifyOrExit(udpEncapHeader.GetSourcePort() > 0 && udpEncapHeader.GetDestinationPort() > 0, error = kErrorDrop);
+
+    VerifyOrExit((message = Get<Ip6::Udp>().NewMessage()) != nullptr, error = kErrorNoBufs);
+    SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, offset, length));
+
+    messageInfo.SetSockPort(udpEncapHeader.GetSourcePort());
     messageInfo.SetSockAddr(mCommissionerAloc.GetAddress());
-    messageInfo.SetPeerPort(tlv.GetDestinationPort());
+    messageInfo.SetPeerPort(udpEncapHeader.GetDestinationPort());
 
     SuccessOrExit(error = Tlv::Find<Ip6AddressTlv>(aMessage, messageInfo.GetPeerAddr()));
 
     SuccessOrExit(error = Get<Ip6::Udp>().SendDatagram(*message, messageInfo, Ip6::kProtoUdp));
-    mUdpProxyPort = tlv.GetSourcePort();
+    mUdpProxyPort = udpEncapHeader.GetSourcePort();
 
     LogInfo("Proxy transmit sent to %s", messageInfo.GetPeerAddr().ToString().AsCString());
 
@@ -357,55 +324,66 @@
     Error          error;
     Coap::Message *message = nullptr;
 
-    VerifyOrExit(aMessageInfo.GetSockAddr() == mCommissionerAloc.GetAddress(),
-                 error = kErrorDestinationAddressFiltered);
+    if (aMessageInfo.GetSockAddr() != mCommissionerAloc.GetAddress())
+    {
+        LogDebg("Filtered out message for commissioner: dest %s != %s (ALOC)",
+                aMessageInfo.GetSockAddr().ToString().AsCString(),
+                mCommissionerAloc.GetAddress().ToString().AsCString());
+        ExitNow(error = kErrorDestinationAddressFiltered);
+    }
 
     VerifyOrExit(aMessage.GetLength() > 0, error = kErrorNone);
 
-    message = Get<Coap::CoapSecure>().NewPriorityNonConfirmablePostMessage(UriPath::kProxyRx);
+    message = Get<Tmf::SecureAgent>().NewPriorityNonConfirmablePostMessage(kUriProxyRx);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     {
-        UdpEncapsulationTlv tlv;
-        uint16_t            offset;
-        uint16_t            udpLength = aMessage.GetLength() - aMessage.GetOffset();
+        ExtendedTlv               extTlv;
+        UdpEncapsulationTlvHeader udpEncapHeader;
+        uint16_t                  udpLength = aMessage.GetLength() - aMessage.GetOffset();
 
-        tlv.Init();
-        tlv.SetSourcePort(aMessageInfo.GetPeerPort());
-        tlv.SetDestinationPort(aMessageInfo.GetSockPort());
-        tlv.SetUdpLength(udpLength);
-        SuccessOrExit(error = message->Append(tlv));
+        extTlv.SetType(Tlv::kUdpEncapsulation);
+        extTlv.SetLength(sizeof(UdpEncapsulationTlvHeader) + udpLength);
+        SuccessOrExit(error = message->Append(extTlv));
 
-        offset = message->GetLength();
-        SuccessOrExit(error = message->SetLength(offset + udpLength));
-        aMessage.CopyTo(aMessage.GetOffset(), offset, udpLength, *message);
+        udpEncapHeader.SetSourcePort(aMessageInfo.GetPeerPort());
+        udpEncapHeader.SetDestinationPort(aMessageInfo.GetSockPort());
+        SuccessOrExit(error = message->Append(udpEncapHeader));
+        SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, aMessage.GetOffset(), udpLength));
     }
 
     SuccessOrExit(error = Tlv::Append<Ip6AddressTlv>(*message, aMessageInfo.GetPeerAddr()));
 
-    SuccessOrExit(error = Get<Coap::CoapSecure>().SendMessage(*message, Get<Coap::CoapSecure>().GetMessageInfo()));
+    SuccessOrExit(error = Get<Tmf::SecureAgent>().SendMessage(*message, Get<Tmf::SecureAgent>().GetMessageInfo()));
 
-    LogInfo("Sent to commissioner on %s", UriPath::kProxyRx);
+    LogInfo("Sent to commissioner on ProxyRx (c/ur)");
 
 exit:
     FreeMessageOnError(message, error);
-    LogError("notify commissioner on ProxyRx (c/ur)", error);
+    if (error != kErrorDestinationAddressFiltered)
+    {
+        LogError("Notify commissioner on ProxyRx (c/ur)", error);
+    }
 
     return error != kErrorDestinationAddressFiltered;
 }
 
-void BorderAgent::HandleRelayReceive(const Coap::Message &aMessage)
+template <> void BorderAgent::HandleTmf<kUriRelayRx>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
+    OT_UNUSED_VARIABLE(aMessageInfo);
+
     Coap::Message *message = nullptr;
-    Error          error;
+    Error          error   = kErrorNone;
+
+    VerifyOrExit(mState != kStateStopped);
 
     VerifyOrExit(aMessage.IsNonConfirmablePostRequest(), error = kErrorDrop);
 
-    message = Get<Coap::CoapSecure>().NewPriorityNonConfirmablePostMessage(UriPath::kRelayRx);
+    message = Get<Tmf::SecureAgent>().NewPriorityNonConfirmablePostMessage(kUriRelayRx);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = ForwardToCommissioner(*message, aMessage));
-    LogInfo("Sent to commissioner on %s", UriPath::kRelayRx);
+    LogInfo("Sent to commissioner on RelayRx (c/rx)");
 
 exit:
     FreeMessageOnError(message, error);
@@ -413,15 +391,12 @@
 
 Error BorderAgent::ForwardToCommissioner(Coap::Message &aForwardMessage, const Message &aMessage)
 {
-    Error    error  = kErrorNone;
-    uint16_t offset = 0;
+    Error error;
 
-    offset = aForwardMessage.GetLength();
-    SuccessOrExit(error = aForwardMessage.SetLength(offset + aMessage.GetLength() - aMessage.GetOffset()));
-    aMessage.CopyTo(aMessage.GetOffset(), offset, aMessage.GetLength() - aMessage.GetOffset(), aForwardMessage);
-
+    SuccessOrExit(error = aForwardMessage.AppendBytesFromMessage(aMessage, aMessage.GetOffset(),
+                                                                 aMessage.GetLength() - aMessage.GetOffset()));
     SuccessOrExit(error =
-                      Get<Coap::CoapSecure>().SendMessage(aForwardMessage, Get<Coap::CoapSecure>().GetMessageInfo()));
+                      Get<Tmf::SecureAgent>().SendMessage(aForwardMessage, Get<Tmf::SecureAgent>().GetMessageInfo()));
 
     LogInfo("Sent to commissioner");
 
@@ -430,77 +405,128 @@
     return error;
 }
 
-void BorderAgent::HandleKeepAlive(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <>
+void BorderAgent::HandleTmf<kUriCommissionerPetition>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    Error error;
-
-    error = ForwardToLeader(aMessage, aMessageInfo, UriPath::kLeaderKeepAlive, false, true);
-
-    if (error == kErrorNone)
-    {
-        mTimer.Start(kKeepAliveTimeout);
-    }
+    IgnoreError(ForwardToLeader(aMessage, aMessageInfo, kUriLeaderPetition));
 }
 
-void BorderAgent::HandleRelayTransmit(const Coap::Message &aMessage)
+template <>
+void BorderAgent::HandleTmf<kUriCommissionerGet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
+    IgnoreError(ForwardToLeader(aMessage, aMessageInfo, kUriCommissionerGet));
+}
+
+template <>
+void BorderAgent::HandleTmf<kUriCommissionerSet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    IgnoreError(ForwardToLeader(aMessage, aMessageInfo, kUriCommissionerSet));
+}
+
+template <> void BorderAgent::HandleTmf<kUriActiveGet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    IgnoreError(ForwardToLeader(aMessage, aMessageInfo, kUriActiveGet));
+}
+
+template <> void BorderAgent::HandleTmf<kUriActiveSet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    IgnoreError(ForwardToLeader(aMessage, aMessageInfo, kUriActiveSet));
+}
+
+template <> void BorderAgent::HandleTmf<kUriPendingGet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    IgnoreError(ForwardToLeader(aMessage, aMessageInfo, kUriPendingGet));
+}
+
+template <> void BorderAgent::HandleTmf<kUriPendingSet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    IgnoreError(ForwardToLeader(aMessage, aMessageInfo, kUriPendingSet));
+}
+
+template <>
+void BorderAgent::HandleTmf<kUriCommissionerKeepAlive>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    VerifyOrExit(mState != kStateStopped);
+
+    SuccessOrExit(ForwardToLeader(aMessage, aMessageInfo, kUriLeaderKeepAlive));
+    mTimer.Start(kKeepAliveTimeout);
+
+exit:
+    return;
+}
+
+template <> void BorderAgent::HandleTmf<kUriRelayTx>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    OT_UNUSED_VARIABLE(aMessageInfo);
+
     Error            error = kErrorNone;
     uint16_t         joinerRouterRloc;
-    Coap::Message *  message = nullptr;
+    Coap::Message   *message = nullptr;
     Tmf::MessageInfo messageInfo(GetInstance());
-    uint16_t         offset = 0;
+
+    VerifyOrExit(mState != kStateStopped);
 
     VerifyOrExit(aMessage.IsNonConfirmablePostRequest());
 
     SuccessOrExit(error = Tlv::Find<JoinerRouterLocatorTlv>(aMessage, joinerRouterRloc));
 
-    message = Get<Tmf::Agent>().NewPriorityNonConfirmablePostMessage(UriPath::kRelayTx);
+    message = Get<Tmf::Agent>().NewPriorityNonConfirmablePostMessage(kUriRelayTx);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
-    offset = message->GetLength();
-    SuccessOrExit(error = message->SetLength(offset + aMessage.GetLength() - aMessage.GetOffset()));
-    aMessage.CopyTo(aMessage.GetOffset(), offset, aMessage.GetLength() - aMessage.GetOffset(), *message);
+    SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, aMessage.GetOffset(),
+                                                          aMessage.GetLength() - aMessage.GetOffset()));
 
     messageInfo.SetSockAddrToRlocPeerAddrTo(joinerRouterRloc);
     messageInfo.SetSockPortToTmf();
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("Sent to joiner router request on %s", UriPath::kRelayTx);
+    LogInfo("Sent to joiner router request on RelayTx (c/tx)");
 
 exit:
     FreeMessageOnError(message, error);
     LogError("send to joiner router request RelayTx (c/tx)", error);
 }
 
-Error BorderAgent::ForwardToLeader(const Coap::Message &   aMessage,
-                                   const Ip6::MessageInfo &aMessageInfo,
-                                   const char *            aPath,
-                                   bool                    aPetition,
-                                   bool                    aSeparate)
+Error BorderAgent::ForwardToLeader(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo, Uri aUri)
 {
     Error            error          = kErrorNone;
-    ForwardContext * forwardContext = nullptr;
+    ForwardContext  *forwardContext = nullptr;
     Tmf::MessageInfo messageInfo(GetInstance());
-    Coap::Message *  message = nullptr;
-    uint16_t         offset  = 0;
+    Coap::Message   *message  = nullptr;
+    bool             petition = false;
+    bool             separate = false;
 
-    if (aSeparate)
+    VerifyOrExit(mState != kStateStopped);
+
+    switch (aUri)
     {
-        SuccessOrExit(error = Get<Coap::CoapSecure>().SendAck(aMessage, aMessageInfo));
+    case kUriLeaderPetition:
+        petition = true;
+        separate = true;
+        break;
+    case kUriLeaderKeepAlive:
+        separate = true;
+        break;
+    default:
+        break;
+    }
+
+    if (separate)
+    {
+        SuccessOrExit(error = Get<Tmf::SecureAgent>().SendAck(aMessage, aMessageInfo));
     }
 
     forwardContext = static_cast<ForwardContext *>(Heap::CAlloc(1, sizeof(ForwardContext)));
     VerifyOrExit(forwardContext != nullptr, error = kErrorNoBufs);
 
-    forwardContext->Init(GetInstance(), aMessage, aPetition, aSeparate);
+    forwardContext->Init(GetInstance(), aMessage, petition, separate);
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(aPath);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(aUri);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
-    offset = message->GetLength();
-    SuccessOrExit(error = message->SetLength(offset + aMessage.GetLength() - aMessage.GetOffset()));
-    aMessage.CopyTo(aMessage.GetOffset(), offset, aMessage.GetLength() - aMessage.GetOffset(), *message);
+    SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, aMessage.GetOffset(),
+                                                          aMessage.GetLength() - aMessage.GetOffset()));
 
     SuccessOrExit(error = messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc());
     messageInfo.SetSockPortToTmf();
@@ -510,7 +536,7 @@
     // HandleCoapResponse is responsible to free this forward context.
     forwardContext = nullptr;
 
-    LogInfo("Forwarded request to leader on %s", aPath);
+    LogInfo("Forwarded request to leader on %s", PathForUri(aUri));
 
 exit:
     LogError("forward to leader", error);
@@ -523,7 +549,7 @@
         }
 
         FreeMessage(message);
-        SendErrorMessage(aMessage, aSeparate, error);
+        SendErrorMessage(aMessage, separate, error);
     }
 
     return error;
@@ -552,38 +578,21 @@
     }
 }
 
-uint16_t BorderAgent::GetUdpPort(void) const
-{
-    return Get<Coap::CoapSecure>().GetUdpPort();
-}
+uint16_t BorderAgent::GetUdpPort(void) const { return Get<Tmf::SecureAgent>().GetUdpPort(); }
 
 void BorderAgent::Start(void)
 {
-    Error             error;
-    Coap::CoapSecure &coaps = Get<Coap::CoapSecure>();
-    Pskc              pskc;
+    Error error;
+    Pskc  pskc;
 
     VerifyOrExit(mState == kStateStopped, error = kErrorNone);
 
     Get<KeyManager>().GetPskc(pskc);
-    SuccessOrExit(error = coaps.Start(kBorderAgentUdpPort));
-    SuccessOrExit(error = coaps.SetPsk(pskc.m8, Pskc::kSize));
+    SuccessOrExit(error = Get<Tmf::SecureAgent>().Start(kBorderAgentUdpPort));
+    SuccessOrExit(error = Get<Tmf::SecureAgent>().SetPsk(pskc.m8, Pskc::kSize));
 
     pskc.Clear();
-    coaps.SetConnectedCallback(HandleConnected, this);
-
-    coaps.AddResource(mActiveGet);
-    coaps.AddResource(mActiveSet);
-    coaps.AddResource(mPendingGet);
-    coaps.AddResource(mPendingSet);
-    coaps.AddResource(mCommissionerPetition);
-    coaps.AddResource(mCommissionerKeepAlive);
-    coaps.AddResource(mCommissionerSet);
-    coaps.AddResource(mCommissionerGet);
-    coaps.AddResource(mProxyTransmit);
-    coaps.AddResource(mRelayTransmit);
-
-    Get<Tmf::Agent>().AddResource(mRelayReceive);
+    Get<Tmf::SecureAgent>().SetConnectedCallback(HandleConnected, this);
 
     mState        = kStateStarted;
     mUdpProxyPort = 0;
@@ -597,42 +606,21 @@
     }
 }
 
-void BorderAgent::HandleTimeout(Timer &aTimer)
-{
-    aTimer.Get<BorderAgent>().HandleTimeout();
-}
-
 void BorderAgent::HandleTimeout(void)
 {
-    if (Get<Coap::CoapSecure>().IsConnected())
+    if (Get<Tmf::SecureAgent>().IsConnected())
     {
-        Get<Coap::CoapSecure>().Disconnect();
+        Get<Tmf::SecureAgent>().Disconnect();
         LogWarn("Reset commissioner session");
     }
 }
 
 void BorderAgent::Stop(void)
 {
-    Coap::CoapSecure &coaps = Get<Coap::CoapSecure>();
-
     VerifyOrExit(mState != kStateStopped);
 
     mTimer.Stop();
-
-    coaps.RemoveResource(mCommissionerPetition);
-    coaps.RemoveResource(mCommissionerKeepAlive);
-    coaps.RemoveResource(mCommissionerSet);
-    coaps.RemoveResource(mCommissionerGet);
-    coaps.RemoveResource(mActiveGet);
-    coaps.RemoveResource(mActiveSet);
-    coaps.RemoveResource(mPendingGet);
-    coaps.RemoveResource(mPendingSet);
-    coaps.RemoveResource(mProxyTransmit);
-    coaps.RemoveResource(mRelayTransmit);
-
-    Get<Tmf::Agent>().RemoveResource(mRelayReceive);
-
-    coaps.Stop();
+    Get<Tmf::SecureAgent>().Stop();
 
     mState        = kStateStopped;
     mUdpProxyPort = 0;
diff --git a/src/core/meshcop/border_agent.hpp b/src/core/meshcop/border_agent.hpp
index 6f6bc45..4a047a5 100644
--- a/src/core/meshcop/border_agent.hpp
+++ b/src/core/meshcop/border_agent.hpp
@@ -40,12 +40,14 @@
 
 #include <openthread/border_agent.h>
 
-#include "coap/coap.hpp"
 #include "common/as_core_type.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "common/notifier.hpp"
+#include "common/settings.hpp"
 #include "net/udp6.hpp"
+#include "thread/tmf.hpp"
+#include "thread/uri_paths.hpp"
 
 namespace ot {
 
@@ -54,6 +56,8 @@
 class BorderAgent : public InstanceLocator, private NonCopyable
 {
     friend class ot::Notifier;
+    friend class Tmf::Agent;
+    friend class Tmf::SecureAgent;
 
 public:
     /**
@@ -75,6 +79,25 @@
      */
     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      A pointer to buffer to receive the ID.
+     * @param[inout] aLength  Specifies the length of `aId` when used as input and receives the length
+     *                        actual ID data copied to `aId` when used as output.
+     *
+     * @retval OT_ERROR_INVALID_ARGS  If value of `aLength` if smaller than `OT_BORDER_AGENT_ID_LENGTH`.
+     * @retval OT_ERROR_NONE          If successfully retrieved the Border Agent ID.
+     *
+     */
+    Error GetId(uint8_t *aId, uint16_t &aLength);
+#endif
+
     /**
      * This method gets the UDP port of this service.
      *
@@ -145,28 +168,18 @@
     static void HandleConnected(bool aConnected, void *aContext);
     void        HandleConnected(bool aConnected);
 
-    template <Coap::Resource BorderAgent::*aResource>
-    static void HandleRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static void HandleTimeout(Timer &aTimer);
-    void        HandleTimeout(void);
+    void HandleTimeout(void);
 
-    static void HandleCoapResponse(void *               aContext,
-                                   otMessage *          aMessage,
+    static void HandleCoapResponse(void                *aContext,
+                                   otMessage           *aMessage,
                                    const otMessageInfo *aMessageInfo,
                                    Error                aResult);
     void        HandleCoapResponse(ForwardContext &aForwardContext, const Coap::Message *aResponse, Error aResult);
 
-    Error       ForwardToLeader(const Coap::Message &   aMessage,
-                                const Ip6::MessageInfo &aMessageInfo,
-                                const char *            aPath,
-                                bool                    aPetition,
-                                bool                    aSeparate);
+    Error       ForwardToLeader(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo, Uri aUri);
     Error       ForwardToCommissioner(Coap::Message &aForwardMessage, const Message &aMessage);
-    void        HandleKeepAlive(const Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    void        HandleRelayTransmit(const Coap::Message &aMessage);
-    void        HandleRelayReceive(const Coap::Message &aMessage);
-    void        HandleProxyTransmit(const Coap::Message &aMessage);
     static bool HandleUdpReceive(void *aContext, const otMessage *aMessage, const otMessageInfo *aMessageInfo)
     {
         return static_cast<BorderAgent *>(aContext)->HandleUdpReceive(AsCoreType(aMessage), AsCoreType(aMessageInfo));
@@ -175,28 +188,34 @@
 
     static constexpr uint32_t kKeepAliveTimeout = 50 * 1000; // Timeout to reject a commissioner.
 
-    Ip6::MessageInfo mMessageInfo;
+    using TimeoutTimer = TimerMilliIn<BorderAgent, &BorderAgent::HandleTimeout>;
 
-    Coap::Resource mCommissionerPetition;
-    Coap::Resource mCommissionerKeepAlive;
-    Coap::Resource mRelayTransmit;
-    Coap::Resource mRelayReceive;
-    Coap::Resource mCommissionerGet;
-    Coap::Resource mCommissionerSet;
-    Coap::Resource mActiveGet;
-    Coap::Resource mActiveSet;
-    Coap::Resource mPendingGet;
-    Coap::Resource mPendingSet;
-    Coap::Resource mProxyTransmit;
+    Ip6::MessageInfo mMessageInfo;
 
     Ip6::Udp::Receiver         mUdpReceiver; ///< The UDP receiver to receive packets from external commissioner
     Ip6::Netif::UnicastAddress mCommissionerAloc;
 
-    TimerMilli mTimer;
-    State      mState;
-    uint16_t   mUdpProxyPort;
+    TimeoutTimer mTimer;
+    State        mState;
+    uint16_t     mUdpProxyPort;
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+    Settings::BorderAgentId mId;
+    bool                    mIdInitialized;
+#endif
 };
 
+DeclareTmfHandler(BorderAgent, kUriRelayRx);
+DeclareTmfHandler(BorderAgent, kUriCommissionerPetition);
+DeclareTmfHandler(BorderAgent, kUriCommissionerKeepAlive);
+DeclareTmfHandler(BorderAgent, kUriRelayTx);
+DeclareTmfHandler(BorderAgent, kUriCommissionerGet);
+DeclareTmfHandler(BorderAgent, kUriCommissionerSet);
+DeclareTmfHandler(BorderAgent, kUriActiveGet);
+DeclareTmfHandler(BorderAgent, kUriActiveSet);
+DeclareTmfHandler(BorderAgent, kUriPendingGet);
+DeclareTmfHandler(BorderAgent, kUriPendingSet);
+DeclareTmfHandler(BorderAgent, kUriProxyTx);
+
 } // namespace MeshCoP
 
 DefineMapEnum(otBorderAgentState, MeshCoP::BorderAgent::State);
diff --git a/src/core/meshcop/commissioner.cpp b/src/core/meshcop/commissioner.cpp
index c1c73eb..0bac432 100644
--- a/src/core/meshcop/commissioner.cpp
+++ b/src/core/meshcop/commissioner.cpp
@@ -64,18 +64,13 @@
     , mJoinerRloc(0)
     , mSessionId(0)
     , mTransmitAttempts(0)
-    , mJoinerExpirationTimer(aInstance, HandleJoinerExpirationTimer)
-    , mTimer(aInstance, HandleTimer)
-    , mRelayReceive(UriPath::kRelayRx, &Commissioner::HandleRelayReceive, this)
-    , mDatasetChanged(UriPath::kDatasetChanged, &Commissioner::HandleDatasetChanged, this)
-    , mJoinerFinalize(UriPath::kJoinerFinalize, &Commissioner::HandleJoinerFinalize, this)
+    , mJoinerExpirationTimer(aInstance)
+    , mTimer(aInstance)
+    , mJoinerSessionTimer(aInstance)
     , mAnnounceBegin(aInstance)
     , mEnergyScan(aInstance)
     , mPanIdQuery(aInstance)
     , mState(kStateDisabled)
-    , mStateCallback(nullptr)
-    , mJoinerCallback(nullptr)
-    , mCallbackContext(nullptr)
 {
     memset(reinterpret_cast<void *>(mJoiners), 0, sizeof(mJoiners));
 
@@ -101,10 +96,7 @@
 
     LogInfo("State: %s -> %s", StateToString(oldState), StateToString(aState));
 
-    if (mStateCallback)
-    {
-        mStateCallback(MapEnum(mState), mCallbackContext);
-    }
+    mStateCallback.InvokeIfSet(MapEnum(mState));
 
 exit:
     return;
@@ -116,7 +108,7 @@
     Mac::ExtAddress joinerId;
     bool            noJoinerId = false;
 
-    VerifyOrExit((mJoinerCallback != nullptr) && (aJoiner != nullptr));
+    VerifyOrExit(mJoinerCallback.IsSet() && (aJoiner != nullptr));
 
     aJoiner->CopyToJoinerInfo(joinerInfo);
 
@@ -133,33 +125,24 @@
         noJoinerId = true;
     }
 
-    mJoinerCallback(MapEnum(aEvent), &joinerInfo, noJoinerId ? nullptr : &joinerId, mCallbackContext);
+    mJoinerCallback.Invoke(MapEnum(aEvent), &joinerInfo, noJoinerId ? nullptr : &joinerId);
 
 exit:
     return;
 }
 
-void Commissioner::AddCoapResources(void)
+void Commissioner::HandleSecureAgentConnected(bool aConnected, void *aContext)
 {
-    Get<Tmf::Agent>().AddResource(mRelayReceive);
-    Get<Tmf::Agent>().AddResource(mDatasetChanged);
-    Get<Coap::CoapSecure>().AddResource(mJoinerFinalize);
+    static_cast<Commissioner *>(aContext)->HandleSecureAgentConnected(aConnected);
 }
 
-void Commissioner::RemoveCoapResources(void)
+void Commissioner::HandleSecureAgentConnected(bool aConnected)
 {
-    Get<Tmf::Agent>().RemoveResource(mRelayReceive);
-    Get<Tmf::Agent>().RemoveResource(mDatasetChanged);
-    Get<Coap::CoapSecure>().RemoveResource(mJoinerFinalize);
-}
+    if (!aConnected)
+    {
+        mJoinerSessionTimer.Stop();
+    }
 
-void Commissioner::HandleCoapsConnected(bool aConnected, void *aContext)
-{
-    static_cast<Commissioner *>(aContext)->HandleCoapsConnected(aConnected);
-}
-
-void Commissioner::HandleCoapsConnected(bool aConnected)
-{
     SignalJoinerEvent(aConnected ? kJoinerEventConnected : kJoinerEventEnd, mActiveJoiner);
 }
 
@@ -229,7 +212,7 @@
 
 Commissioner::Joiner *Commissioner::FindBestMatchingJoinerEntry(const Mac::ExtAddress &aReceivedJoinerId)
 {
-    Joiner *        best = nullptr;
+    Joiner         *best = nullptr;
     Mac::ExtAddress joinerId;
 
     // Prefer a full Joiner ID match, if not found use the entry
@@ -309,12 +292,11 @@
     Get<BorderAgent>().Stop();
 #endif
 
-    SuccessOrExit(error = Get<Coap::CoapSecure>().Start(SendRelayTransmit, this));
-    Get<Coap::CoapSecure>().SetConnectedCallback(&Commissioner::HandleCoapsConnected, this);
+    SuccessOrExit(error = Get<Tmf::SecureAgent>().Start(SendRelayTransmit, this));
+    Get<Tmf::SecureAgent>().SetConnectedCallback(&Commissioner::HandleSecureAgentConnected, this);
 
-    mStateCallback    = aStateCallback;
-    mJoinerCallback   = aJoinerCallback;
-    mCallbackContext  = aCallbackContext;
+    mStateCallback.Set(aStateCallback, aCallbackContext);
+    mJoinerCallback.Set(aJoinerCallback, aCallbackContext);
     mTransmitAttempts = 0;
 
     SuccessOrExit(error = SendPetition());
@@ -325,7 +307,7 @@
 exit:
     if ((error != kErrorNone) && (error != kErrorAlready))
     {
-        Get<Coap::CoapSecure>().Stop();
+        Get<Tmf::SecureAgent>().Stop();
     }
 
     LogError("start commissioner", error);
@@ -339,12 +321,12 @@
 
     VerifyOrExit(mState != kStateDisabled, error = kErrorAlready);
 
-    Get<Coap::CoapSecure>().Stop();
+    mJoinerSessionTimer.Stop();
+    Get<Tmf::SecureAgent>().Stop();
 
     if (mState == kStateActive)
     {
         Get<ThreadNetif>().RemoveUnicastAddress(mCommissionerAloc);
-        RemoveCoapResources();
         ClearJoiners();
         needResign = true;
     }
@@ -455,7 +437,7 @@
 
 Error Commissioner::AddJoiner(const Mac::ExtAddress *aEui64,
                               const JoinerDiscerner *aDiscerner,
-                              const char *           aPskd,
+                              const char            *aPskd,
                               uint32_t               aTimeout)
 {
     Error   error = kErrorNone;
@@ -628,11 +610,6 @@
     return error;
 }
 
-void Commissioner::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Commissioner>().HandleTimer();
-}
-
 void Commissioner::HandleTimer(void)
 {
     switch (mState)
@@ -650,11 +627,6 @@
     }
 }
 
-void Commissioner::HandleJoinerExpirationTimer(Timer &aTimer)
-{
-    aTimer.Get<Commissioner>().HandleJoinerExpirationTimer();
-}
-
 void Commissioner::HandleJoinerExpirationTimer(void)
 {
     TimeMilli now = TimerMilli::GetNow();
@@ -688,14 +660,7 @@
             continue;
         }
 
-        if (joiner.mExpirationTime <= now)
-        {
-            next = now;
-        }
-        else if (joiner.mExpirationTime < next)
-        {
-            next = joiner.mExpirationTime;
-        }
+        next = Min(next, Max(now, joiner.mExpirationTime));
     }
 
     if (next < now.GetDistantFuture())
@@ -711,11 +676,11 @@
 Error Commissioner::SendMgmtCommissionerGetRequest(const uint8_t *aTlvs, uint8_t aLength)
 {
     Error            error = kErrorNone;
-    Coap::Message *  message;
+    Coap::Message   *message;
     Tmf::MessageInfo messageInfo(GetInstance());
     Tlv              tlv;
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kCommissionerGet);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriCommissionerGet);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     if (aLength > 0)
@@ -730,15 +695,15 @@
     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);
     return error;
 }
 
-void Commissioner::HandleMgmtCommissionerGetResponse(void *               aContext,
-                                                     otMessage *          aMessage,
+void Commissioner::HandleMgmtCommissionerGetResponse(void                *aContext,
+                                                     otMessage           *aMessage,
                                                      const otMessageInfo *aMessageInfo,
                                                      Error                aResult)
 {
@@ -746,14 +711,14 @@
                                                                              AsCoreTypePtr(aMessageInfo), aResult);
 }
 
-void Commissioner::HandleMgmtCommissionerGetResponse(Coap::Message *         aMessage,
+void Commissioner::HandleMgmtCommissionerGetResponse(Coap::Message          *aMessage,
                                                      const Ip6::MessageInfo *aMessageInfo,
                                                      Error                   aResult)
 {
     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;
@@ -762,10 +727,10 @@
 Error Commissioner::SendMgmtCommissionerSetRequest(const Dataset &aDataset, const uint8_t *aTlvs, uint8_t aLength)
 {
     Error            error = kErrorNone;
-    Coap::Message *  message;
+    Coap::Message   *message;
     Tmf::MessageInfo messageInfo(GetInstance());
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kCommissionerSet);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriCommissionerSet);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     if (aDataset.IsLocatorSet())
@@ -799,15 +764,15 @@
     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);
     return error;
 }
 
-void Commissioner::HandleMgmtCommissionerSetResponse(void *               aContext,
-                                                     otMessage *          aMessage,
+void Commissioner::HandleMgmtCommissionerSetResponse(void                *aContext,
+                                                     otMessage           *aMessage,
                                                      const otMessageInfo *aMessageInfo,
                                                      Error                aResult)
 {
@@ -815,14 +780,14 @@
                                                                              AsCoreTypePtr(aMessageInfo), aResult);
 }
 
-void Commissioner::HandleMgmtCommissionerSetResponse(Coap::Message *         aMessage,
+void Commissioner::HandleMgmtCommissionerSetResponse(Coap::Message          *aMessage,
                                                      const Ip6::MessageInfo *aMessageInfo,
                                                      Error                   aResult)
 {
     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;
@@ -831,13 +796,13 @@
 Error Commissioner::SendPetition(void)
 {
     Error             error   = kErrorNone;
-    Coap::Message *   message = nullptr;
+    Coap::Message    *message = nullptr;
     Tmf::MessageInfo  messageInfo(GetInstance());
     CommissionerIdTlv commissionerIdTlv;
 
     mTransmitAttempts++;
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kLeaderPetition);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriLeaderPetition);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     commissionerIdTlv.Init();
@@ -848,15 +813,15 @@
     SuccessOrExit(
         error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, Commissioner::HandleLeaderPetitionResponse, this));
 
-    LogInfo("sent petition");
+    LogInfo("Sent %s", UriToString<kUriLeaderPetition>());
 
 exit:
     FreeMessageOnError(message, error);
     return error;
 }
 
-void Commissioner::HandleLeaderPetitionResponse(void *               aContext,
-                                                otMessage *          aMessage,
+void Commissioner::HandleLeaderPetitionResponse(void                *aContext,
+                                                otMessage           *aMessage,
                                                 const otMessageInfo *aMessageInfo,
                                                 Error                aResult)
 {
@@ -864,7 +829,7 @@
                                                                         AsCoreTypePtr(aMessageInfo), aResult);
 }
 
-void Commissioner::HandleLeaderPetitionResponse(Coap::Message *         aMessage,
+void Commissioner::HandleLeaderPetitionResponse(Coap::Message          *aMessage,
                                                 const Ip6::MessageInfo *aMessageInfo,
                                                 Error                   aResult)
 {
@@ -877,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)));
@@ -895,7 +860,6 @@
     IgnoreError(Get<Mle::MleRouter>().GetCommissionerAloc(mCommissionerAloc.GetAddress(), mSessionId));
     Get<ThreadNetif>().AddUnicastAddress(mCommissionerAloc);
 
-    AddCoapResources();
     SetState(kStateActive);
 
     mTransmitAttempts = 0;
@@ -916,18 +880,15 @@
     }
 }
 
-void Commissioner::SendKeepAlive(void)
-{
-    SendKeepAlive(mSessionId);
-}
+void Commissioner::SendKeepAlive(void) { SendKeepAlive(mSessionId); }
 
 void Commissioner::SendKeepAlive(uint16_t aSessionId)
 {
     Error            error   = kErrorNone;
-    Coap::Message *  message = nullptr;
+    Coap::Message   *message = nullptr;
     Tmf::MessageInfo messageInfo(GetInstance());
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kLeaderKeepAlive);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriLeaderKeepAlive);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(
@@ -939,15 +900,15 @@
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo,
                                                         Commissioner::HandleLeaderKeepAliveResponse, this));
 
-    LogInfo("sent keep alive");
+    LogInfo("Sent %s", UriToString<kUriLeaderKeepAlive>());
 
 exit:
     FreeMessageOnError(message, error);
     LogError("send keep alive", error);
 }
 
-void Commissioner::HandleLeaderKeepAliveResponse(void *               aContext,
-                                                 otMessage *          aMessage,
+void Commissioner::HandleLeaderKeepAliveResponse(void                *aContext,
+                                                 otMessage           *aMessage,
                                                  const otMessageInfo *aMessageInfo,
                                                  Error                aResult)
 {
@@ -955,7 +916,7 @@
                                                                          AsCoreTypePtr(aMessageInfo), aResult);
 }
 
-void Commissioner::HandleLeaderKeepAliveResponse(Coap::Message *         aMessage,
+void Commissioner::HandleLeaderKeepAliveResponse(Coap::Message          *aMessage,
                                                  const Ip6::MessageInfo *aMessageInfo,
                                                  Error                   aResult)
 {
@@ -967,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)));
@@ -978,12 +939,7 @@
     return;
 }
 
-void Commissioner::HandleRelayReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<Commissioner *>(aContext)->HandleRelayReceive(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void Commissioner::HandleRelayReceive(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void Commissioner::HandleTmf<kUriRelayRx>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     OT_UNUSED_VARIABLE(aMessageInfo);
 
@@ -1006,10 +962,10 @@
     SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Tlv::kJoinerDtlsEncapsulation, offset, length));
     VerifyOrExit(length <= aMessage.GetLength() - offset, error = kErrorParse);
 
-    if (!Get<Coap::CoapSecure>().IsConnectionActive())
+    if (!Get<Tmf::SecureAgent>().IsConnectionActive())
     {
         Mac::ExtAddress receivedId;
-        Joiner *        joiner;
+        Joiner         *joiner;
 
         mJoinerIid = joinerIid;
         mJoinerIid.ConvertToExtAddress(receivedId);
@@ -1017,21 +973,29 @@
         joiner = FindBestMatchingJoinerEntry(receivedId);
         VerifyOrExit(joiner != nullptr);
 
-        Get<Coap::CoapSecure>().SetPsk(joiner->mPskd);
+        Get<Tmf::SecureAgent>().SetPsk(joiner->mPskd);
         mActiveJoiner = joiner;
 
+        mJoinerSessionTimer.Start(kJoinerSessionTimeoutMillis);
+
         LogJoinerEntry("Starting new session with", *joiner);
         SignalJoinerEvent(kJoinerEventStart, joiner);
     }
     else
     {
-        VerifyOrExit(mJoinerIid == joinerIid);
+        if (mJoinerIid != joinerIid)
+        {
+            LogNote("Ignore %s (%s, 0x%04x), session in progress with (%s, 0x%04x)", UriToString<kUriRelayRx>(),
+                    joinerIid.ToString().AsCString(), joinerRloc, mJoinerIid.ToString().AsCString(), mJoinerRloc);
+
+            ExitNow();
+        }
     }
 
     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));
@@ -1040,54 +1004,64 @@
     joinerMessageInfo.GetPeerAddr().SetIid(mJoinerIid);
     joinerMessageInfo.SetPeerPort(mJoinerPort);
 
-    Get<Coap::CoapSecure>().HandleUdpReceive(aMessage, joinerMessageInfo);
+    Get<Tmf::SecureAgent>().HandleUdpReceive(aMessage, joinerMessageInfo);
 
 exit:
     return;
 }
 
-void Commissioner::HandleDatasetChanged(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
+void Commissioner::HandleJoinerSessionTimer(void)
 {
-    static_cast<Commissioner *>(aContext)->HandleDatasetChanged(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
+    if (mActiveJoiner != nullptr)
+    {
+        LogJoinerEntry("Timed out session with", *mActiveJoiner);
+    }
+
+    Get<Tmf::SecureAgent>().Disconnect();
 }
 
-void Commissioner::HandleDatasetChanged(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <>
+void Commissioner::HandleTmf<kUriDatasetChanged>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
+    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;
 }
 
-void Commissioner::HandleJoinerFinalize(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<Commissioner *>(aContext)->HandleJoinerFinalize(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void Commissioner::HandleJoinerFinalize(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <>
+void Commissioner::HandleTmf<kUriJoinerFinalize>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     OT_UNUSED_VARIABLE(aMessageInfo);
 
-    StateTlv::State    state = StateTlv::kAccept;
-    ProvisioningUrlTlv provisioningUrl;
+    StateTlv::State                state = StateTlv::kAccept;
+    ProvisioningUrlTlv::StringType provisioningUrl;
 
-    LogInfo("received joiner finalize");
+    VerifyOrExit(mState == kStateActive);
 
-    if (Tlv::FindTlv(aMessage, provisioningUrl) == kErrorNone)
+    LogInfo("Received %s", UriToString<kUriJoinerFinalize>());
+
+    switch (Tlv::Find<ProvisioningUrlTlv>(aMessage, provisioningUrl))
     {
-        uint8_t len = static_cast<uint8_t>(StringLength(mProvisioningUrl, sizeof(mProvisioningUrl)));
-
-        if ((provisioningUrl.GetProvisioningUrlLength() != len) ||
-            !memcmp(provisioningUrl.GetProvisioningUrl(), mProvisioningUrl, len))
+    case kErrorNone:
+        if (!StringMatch(provisioningUrl, mProvisioningUrl))
         {
             state = StateTlv::kReject;
         }
+        break;
+
+    case kErrorNotFound:
+        break;
+
+    default:
+        ExitNow();
     }
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
@@ -1101,15 +1075,18 @@
 #endif
 
     SendJoinFinalizeResponse(aMessage, state);
+
+exit:
+    return;
 }
 
 void Commissioner::SendJoinFinalizeResponse(const Coap::Message &aRequest, StateTlv::State aState)
 {
     Error            error = kErrorNone;
     Ip6::MessageInfo joinerMessageInfo;
-    Coap::Message *  message;
+    Coap::Message   *message;
 
-    message = Get<Coap::CoapSecure>().NewPriorityResponseMessage(aRequest);
+    message = Get<Tmf::SecureAgent>().NewPriorityResponseMessage(aRequest);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     message->SetOffset(message->GetLength());
@@ -1129,7 +1106,7 @@
     DumpCert("[THCI] direction=send | type=JOIN_FIN.rsp |", buf, message->GetLength() - message->GetOffset());
 #endif
 
-    SuccessOrExit(error = Get<Coap::CoapSecure>().SendMessage(*message, joinerMessageInfo));
+    SuccessOrExit(error = Get<Tmf::SecureAgent>().SendMessage(*message, joinerMessageInfo));
 
     SignalJoinerEvent(kJoinerEventFinalize, mActiveJoiner);
 
@@ -1139,7 +1116,7 @@
         RemoveJoiner(*mActiveJoiner, kRemoveJoinerDelay);
     }
 
-    LogInfo("sent joiner finalize response");
+    LogInfo("Sent %s response", UriToString<kUriJoinerFinalize>());
 
 exit:
     FreeMessageOnError(message, error);
@@ -1156,14 +1133,13 @@
 
     Error            error = kErrorNone;
     ExtendedTlv      tlv;
-    Coap::Message *  message;
-    uint16_t         offset;
+    Coap::Message   *message;
     Tmf::MessageInfo messageInfo(GetInstance());
     Kek              kek;
 
     Get<KeyManager>().ExtractKek(kek);
 
-    message = Get<Tmf::Agent>().NewPriorityNonConfirmablePostMessage(UriPath::kRelayTx);
+    message = Get<Tmf::Agent>().NewPriorityNonConfirmablePostMessage(kUriRelayTx);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = Tlv::Append<JoinerUdpPortTlv>(*message, mJoinerPort));
@@ -1178,9 +1154,7 @@
     tlv.SetType(Tlv::kJoinerDtlsEncapsulation);
     tlv.SetLength(aMessage.GetLength());
     SuccessOrExit(error = message->Append(tlv));
-    offset = message->GetLength();
-    SuccessOrExit(error = message->SetLength(offset + aMessage.GetLength()));
-    aMessage.CopyTo(0, offset, aMessage.GetLength(), *message);
+    SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, 0, aMessage.GetLength()));
 
     messageInfo.SetSockAddrToRlocPeerAddrTo(mJoinerRloc);
 
@@ -1249,9 +1223,7 @@
 
 #else
 
-void Commissioner::LogJoinerEntry(const char *, const Joiner &) const
-{
-}
+void Commissioner::LogJoinerEntry(const char *, const Joiner &) const {}
 
 #endif // OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
diff --git a/src/core/meshcop/commissioner.hpp b/src/core/meshcop/commissioner.hpp
index 4c1d190..22e061f 100644
--- a/src/core/meshcop/commissioner.hpp
+++ b/src/core/meshcop/commissioner.hpp
@@ -40,9 +40,9 @@
 
 #include <openthread/commissioner.h>
 
-#include "coap/coap.hpp"
 #include "coap/coap_secure.hpp"
 #include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/clearable.hpp"
 #include "common/locator.hpp"
 #include "common/log.hpp"
@@ -57,6 +57,7 @@
 #include "net/udp6.hpp"
 #include "thread/key_manager.hpp"
 #include "thread/mle.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -64,6 +65,9 @@
 
 class Commissioner : public InstanceLocator, private NonCopyable
 {
+    friend class Tmf::Agent;
+    friend class Tmf::SecureAgent;
+
 public:
     /**
      * This enumeration type represents the Commissioner State.
@@ -495,6 +499,9 @@
     static constexpr uint32_t kKeepAliveTimeout     = 50; // TIMEOUT_COMM_PET (seconds)
     static constexpr uint32_t kRemoveJoinerDelay    = 20; // Delay to remove successfully joined joiner
 
+    static constexpr uint32_t kJoinerSessionTimeoutMillis =
+        1000 * OPENTHREAD_CONFIG_COMMISSIONER_JOINER_SESSION_TIMEOUT; // Expiration time for active Joiner session
+
     enum ResignMode : uint8_t
     {
         kSendKeepAliveToResign,
@@ -534,58 +541,49 @@
 
     Error AddJoiner(const Mac::ExtAddress *aEui64,
                     const JoinerDiscerner *aDiscerner,
-                    const char *           aPskd,
+                    const char            *aPskd,
                     uint32_t               aTimeout);
     Error RemoveJoiner(const Mac::ExtAddress *aEui64, const JoinerDiscerner *aDiscerner, uint32_t aDelay);
     void  RemoveJoiner(Joiner &aJoiner, uint32_t aDelay);
 
-    void AddCoapResources(void);
-    void RemoveCoapResources(void);
-
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
-
-    static void HandleJoinerExpirationTimer(Timer &aTimer);
-    void        HandleJoinerExpirationTimer(void);
+    void HandleTimer(void);
+    void HandleJoinerExpirationTimer(void);
 
     void UpdateJoinerExpirationTimer(void);
 
-    static void HandleMgmtCommissionerSetResponse(void *               aContext,
-                                                  otMessage *          aMessage,
+    static void HandleMgmtCommissionerSetResponse(void                *aContext,
+                                                  otMessage           *aMessage,
                                                   const otMessageInfo *aMessageInfo,
                                                   Error                aResult);
-    void        HandleMgmtCommissionerSetResponse(Coap::Message *         aMessage,
+    void        HandleMgmtCommissionerSetResponse(Coap::Message          *aMessage,
                                                   const Ip6::MessageInfo *aMessageInfo,
                                                   Error                   aResult);
-    static void HandleMgmtCommissionerGetResponse(void *               aContext,
-                                                  otMessage *          aMessage,
+    static void HandleMgmtCommissionerGetResponse(void                *aContext,
+                                                  otMessage           *aMessage,
                                                   const otMessageInfo *aMessageInfo,
                                                   Error                aResult);
-    void        HandleMgmtCommissionerGetResponse(Coap::Message *         aMessage,
+    void        HandleMgmtCommissionerGetResponse(Coap::Message          *aMessage,
                                                   const Ip6::MessageInfo *aMessageInfo,
                                                   Error                   aResult);
-    static void HandleLeaderPetitionResponse(void *               aContext,
-                                             otMessage *          aMessage,
+    static void HandleLeaderPetitionResponse(void                *aContext,
+                                             otMessage           *aMessage,
                                              const otMessageInfo *aMessageInfo,
                                              Error                aResult);
     void HandleLeaderPetitionResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult);
-    static void HandleLeaderKeepAliveResponse(void *               aContext,
-                                              otMessage *          aMessage,
+    static void HandleLeaderKeepAliveResponse(void                *aContext,
+                                              otMessage           *aMessage,
                                               const otMessageInfo *aMessageInfo,
                                               Error                aResult);
     void HandleLeaderKeepAliveResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult);
 
-    static void HandleCoapsConnected(bool aConnected, void *aContext);
-    void        HandleCoapsConnected(bool aConnected);
+    static void HandleSecureAgentConnected(bool aConnected, void *aContext);
+    void        HandleSecureAgentConnected(bool aConnected);
 
-    static void HandleRelayReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleRelayReceive(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static void HandleDatasetChanged(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleDatasetChanged(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    void HandleRelayReceive(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static void HandleJoinerFinalize(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleJoinerFinalize(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    void HandleJoinerSessionTimer(void);
 
     void SendJoinFinalizeResponse(const Coap::Message &aRequest, StateTlv::State aState);
 
@@ -604,20 +602,21 @@
 
     static const char *StateToString(State aState);
 
+    using JoinerExpirationTimer = TimerMilliIn<Commissioner, &Commissioner::HandleJoinerExpirationTimer>;
+    using CommissionerTimer     = TimerMilliIn<Commissioner, &Commissioner::HandleTimer>;
+    using JoinerSessionTimer    = TimerMilliIn<Commissioner, &Commissioner::HandleJoinerSessionTimer>;
+
     Joiner mJoiners[OPENTHREAD_CONFIG_COMMISSIONER_MAX_JOINER_ENTRIES];
 
-    Joiner *                 mActiveJoiner;
+    Joiner                  *mActiveJoiner;
     Ip6::InterfaceIdentifier mJoinerIid;
     uint16_t                 mJoinerPort;
     uint16_t                 mJoinerRloc;
     uint16_t                 mSessionId;
     uint8_t                  mTransmitAttempts;
-    TimerMilli               mJoinerExpirationTimer;
-    TimerMilli               mTimer;
-
-    Coap::Resource mRelayReceive;
-    Coap::Resource mDatasetChanged;
-    Coap::Resource mJoinerFinalize;
+    JoinerExpirationTimer    mJoinerExpirationTimer;
+    CommissionerTimer        mTimer;
+    JoinerSessionTimer       mJoinerSessionTimer;
 
     AnnounceBeginClient mAnnounceBegin;
     EnergyScanClient    mEnergyScan;
@@ -630,11 +629,14 @@
 
     State mState;
 
-    StateCallback  mStateCallback;
-    JoinerCallback mJoinerCallback;
-    void *         mCallbackContext;
+    Callback<StateCallback>  mStateCallback;
+    Callback<JoinerCallback> mJoinerCallback;
 };
 
+DeclareTmfHandler(Commissioner, kUriDatasetChanged);
+DeclareTmfHandler(Commissioner, kUriRelayRx);
+DeclareTmfHandler(Commissioner, kUriJoinerFinalize);
+
 } // namespace MeshCoP
 
 DefineMapEnum(otCommissionerState, MeshCoP::Commissioner::State);
diff --git a/src/core/meshcop/dataset.cpp b/src/core/meshcop/dataset.cpp
index 0478085..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;
@@ -162,10 +162,7 @@
     memset(mTlvs, 0, sizeof(mTlvs));
 }
 
-void Dataset::Clear(void)
-{
-    mLength = 0;
-}
+void Dataset::Clear(void) { mLength = 0; }
 
 bool Dataset::IsValid(void) const
 {
@@ -182,10 +179,7 @@
     return rval;
 }
 
-const Tlv *Dataset::GetTlv(Tlv::Type aType) const
-{
-    return Tlv::FindTlv(mTlvs, mLength, aType);
-}
+const Tlv *Dataset::GetTlv(Tlv::Type aType) const { return Tlv::FindTlv(mTlvs, mLength, aType); }
 
 void Dataset::ConvertTo(Info &aDatasetInfo) const
 {
@@ -402,7 +396,7 @@
 {
     Error    error          = kErrorNone;
     uint16_t bytesAvailable = sizeof(mTlvs) - mLength;
-    Tlv *    old            = GetTlv(aType);
+    Tlv     *old            = GetTlv(aType);
     Tlv      tlv;
 
     if (old != nullptr)
@@ -431,15 +425,14 @@
     return error;
 }
 
-Error Dataset::SetTlv(const Tlv &aTlv)
-{
-    return SetTlv(aTlv.GetType(), aTlv.GetValue(), aTlv.GetLength());
-}
+Error Dataset::SetTlv(const Tlv &aTlv) { return SetTlv(aTlv.GetType(), aTlv.GetValue(), aTlv.GetLength()); }
 
-Error Dataset::ReadFromMessage(const Message &aMessage, uint16_t aOffset, uint8_t aLength)
+Error Dataset::ReadFromMessage(const Message &aMessage, uint16_t aOffset, uint16_t aLength)
 {
     Error error = kErrorParse;
 
+    VerifyOrExit(aLength <= kMaxSize);
+
     SuccessOrExit(aMessage.Read(aOffset, mTlvs, aLength));
     mLength = aLength;
 
@@ -521,7 +514,7 @@
 
 Error Dataset::ApplyConfiguration(Instance &aInstance, bool *aIsNetworkKeyUpdated) const
 {
-    Mac::Mac &  mac        = aInstance.Get<Mac::Mac>();
+    Mac::Mac   &mac        = aInstance.Get<Mac::Mac>();
     KeyManager &keyManager = aInstance.Get<KeyManager>();
     Error       error      = kErrorNone;
 
@@ -610,10 +603,7 @@
     RemoveTlv(Tlv::kDelayTimer);
 }
 
-const char *Dataset::TypeToString(Type aType)
-{
-    return (aType == kActive) ? "Active" : "Pending";
-}
+const char *Dataset::TypeToString(Type aType) { return (aType == kActive) ? "Active" : "Pending"; }
 
 } // namespace MeshCoP
 } // namespace ot
diff --git a/src/core/meshcop/dataset.hpp b/src/core/meshcop/dataset.hpp
index 3809513..d45ea02 100644
--- a/src/core/meshcop/dataset.hpp
+++ b/src/core/meshcop/dataset.hpp
@@ -795,7 +795,7 @@
      * @retval kErrorParse   Could not read or parse the dataset from @p aMessage.
      *
      */
-    Error ReadFromMessage(const Message &aMessage, uint16_t aOffset, uint8_t aLength);
+    Error ReadFromMessage(const Message &aMessage, uint16_t aOffset, uint16_t aLength);
 
     /**
      * This method sets the Dataset using an existing Dataset.
diff --git a/src/core/meshcop/dataset_local.cpp b/src/core/meshcop/dataset_local.cpp
index dfcb077..58370c3 100644
--- a/src/core/meshcop/dataset_local.cpp
+++ b/src/core/meshcop/dataset_local.cpp
@@ -237,7 +237,7 @@
     KeyRef         networkKeyRef = IsActive() ? kActiveDatasetNetworkKeyRef : kPendingDatasetNetworkKeyRef;
     KeyRef         pskcRef       = IsActive() ? kActiveDatasetPskcRef : kPendingDatasetPskcRef;
     NetworkKeyTlv *networkKeyTlv = aDataset.GetTlv<NetworkKeyTlv>();
-    PskcTlv *      pskcTlv       = aDataset.GetTlv<PskcTlv>();
+    PskcTlv       *pskcTlv       = aDataset.GetTlv<PskcTlv>();
 
     if (networkKeyTlv != nullptr)
     {
@@ -269,7 +269,7 @@
     KeyRef         networkKeyRef = IsActive() ? kActiveDatasetNetworkKeyRef : kPendingDatasetNetworkKeyRef;
     KeyRef         pskcRef       = IsActive() ? kActiveDatasetPskcRef : kPendingDatasetPskcRef;
     NetworkKeyTlv *networkKeyTlv = aDataset.GetTlv<NetworkKeyTlv>();
-    PskcTlv *      pskcTlv       = aDataset.GetTlv<PskcTlv>();
+    PskcTlv       *pskcTlv       = aDataset.GetTlv<PskcTlv>();
     bool           moveKeys      = false;
     size_t         keyLen;
     Error          error;
diff --git a/src/core/meshcop/dataset_manager.cpp b/src/core/meshcop/dataset_manager.cpp
index 355c4b0..7dcef3d 100644
--- a/src/core/meshcop/dataset_manager.cpp
+++ b/src/core/meshcop/dataset_manager.cpp
@@ -59,16 +59,11 @@
     , mTimestampValid(false)
     , mMgmtPending(false)
     , mTimer(aInstance, aTimerHandler)
-    , mMgmtSetCallback(nullptr)
-    , mMgmtSetCallbackContext(nullptr)
 {
     mTimestamp.Clear();
 }
 
-const Timestamp *DatasetManager::GetTimestamp(void) const
-{
-    return mTimestampValid ? &mTimestamp : nullptr;
-}
+const Timestamp *DatasetManager::GetTimestamp(void) const { return mTimestampValid ? &mTimestamp : nullptr; }
 
 Error DatasetManager::Restore(void)
 {
@@ -115,16 +110,13 @@
     SignalDatasetChange();
 }
 
-void DatasetManager::HandleDetach(void)
-{
-    IgnoreError(Restore());
-}
+void DatasetManager::HandleDetach(void) { IgnoreError(Restore()); }
 
 Error DatasetManager::Save(const Dataset &aDataset)
 {
     Error error = kErrorNone;
     int   compare;
-    bool  isNetworkkeyUpdated = false;
+    bool  isNetworkKeyUpdated = false;
 
     if (aDataset.GetTimestamp(GetType(), mTimestamp) == kErrorNone)
     {
@@ -132,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));
 
@@ -246,15 +238,12 @@
     return error;
 }
 
-void DatasetManager::HandleTimer(void)
-{
-    SendSet();
-}
+void DatasetManager::HandleTimer(void) { SendSet(); }
 
 void DatasetManager::SendSet(void)
 {
     Error            error;
-    Coap::Message *  message = nullptr;
+    Coap::Message   *message = nullptr;
     Tmf::MessageInfo messageInfo(GetInstance());
     Dataset          dataset;
 
@@ -278,8 +267,7 @@
         }
     }
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(IsActiveDataset() ? UriPath::kActiveSet
-                                                                                    : UriPath::kPendingSet);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(IsActiveDataset() ? kUriActiveSet : kUriPendingSet);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     IgnoreError(Read(dataset));
@@ -310,8 +298,8 @@
     }
 }
 
-void DatasetManager::HandleMgmtSetResponse(void *               aContext,
-                                           otMessage *          aMessage,
+void DatasetManager::HandleMgmtSetResponse(void                *aContext,
+                                           otMessage           *aMessage,
                                            const otMessageInfo *aMessageInfo,
                                            Error                aError)
 {
@@ -323,13 +311,13 @@
 {
     OT_UNUSED_VARIABLE(aMessageInfo);
 
-    Error    error;
-    StateTlv stateTlv;
+    Error   error;
+    uint8_t state;
 
     SuccessOrExit(error = aError);
-    VerifyOrExit(Tlv::FindTlv(*aMessage, stateTlv) == kErrorNone, error = kErrorParse);
+    VerifyOrExit(Tlv::Find<StateTlv>(*aMessage, state) == kErrorNone, error = kErrorParse);
 
-    switch (stateTlv.GetState())
+    switch (state)
     {
     case StateTlv::kReject:
         error = kErrorRejected;
@@ -347,15 +335,12 @@
 
     mMgmtPending = false;
 
-    if (mMgmtSetCallback != nullptr)
+    if (mMgmtSetCallback.IsSet())
     {
-        otDatasetMgmtSetCallback callback = mMgmtSetCallback;
-        void *                   context  = mMgmtSetCallbackContext;
+        Callback<otDatasetMgmtSetCallback> callbackCopy = mMgmtSetCallback;
 
-        mMgmtSetCallback        = nullptr;
-        mMgmtSetCallbackContext = nullptr;
-
-        callback(error, context);
+        mMgmtSetCallback.Clear();
+        callbackCopy.Invoke(error);
     }
 
     mTimer.Start(kSendSetDelay);
@@ -406,9 +391,9 @@
     SendGetResponse(aMessage, aMessageInfo, tlvs, length);
 }
 
-void DatasetManager::SendGetResponse(const Coap::Message &   aRequest,
+void DatasetManager::SendGetResponse(const Coap::Message    &aRequest,
                                      const Ip6::MessageInfo &aMessageInfo,
-                                     uint8_t *               aTlvs,
+                                     uint8_t                *aTlvs,
                                      uint8_t                 aLength) const
 {
     Error          error = kErrorNone;
@@ -469,20 +454,19 @@
     return error;
 }
 
-Error DatasetManager::SendSetRequest(const Dataset::Info &    aDatasetInfo,
-                                     const uint8_t *          aTlvs,
+Error DatasetManager::SendSetRequest(const Dataset::Info     &aDatasetInfo,
+                                     const uint8_t           *aTlvs,
                                      uint8_t                  aLength,
                                      otDatasetMgmtSetCallback aCallback,
-                                     void *                   aContext)
+                                     void                    *aContext)
 {
     Error            error   = kErrorNone;
-    Coap::Message *  message = nullptr;
+    Coap::Message   *message = nullptr;
     Tmf::MessageInfo messageInfo(GetInstance());
 
     VerifyOrExit(!mMgmtPending, error = kErrorBusy);
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(IsActiveDataset() ? UriPath::kActiveSet
-                                                                                    : UriPath::kPendingSet);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(IsActiveDataset() ? kUriActiveSet : kUriPendingSet);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
 #if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD
@@ -521,9 +505,8 @@
     IgnoreError(messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc());
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, HandleMgmtSetResponse, this));
-    mMgmtSetCallback        = aCallback;
-    mMgmtSetCallbackContext = aContext;
-    mMgmtPending            = true;
+    mMgmtSetCallback.Set(aCallback, aContext);
+    mMgmtPending = true;
 
     LogInfo("sent dataset set request to leader");
 
@@ -533,12 +516,12 @@
 }
 
 Error DatasetManager::SendGetRequest(const Dataset::Components &aDatasetComponents,
-                                     const uint8_t *            aTlvTypes,
+                                     const uint8_t             *aTlvTypes,
                                      uint8_t                    aLength,
-                                     const otIp6Address *       aAddress) const
+                                     const otIp6Address        *aAddress) const
 {
     Error            error = kErrorNone;
-    Coap::Message *  message;
+    Coap::Message   *message;
     Tmf::MessageInfo messageInfo(GetInstance());
     Tlv              tlv;
     uint8_t          datasetTlvs[kMaxDatasetTlvs];
@@ -606,8 +589,7 @@
         datasetTlvs[length++] = Tlv::kChannelMask;
     }
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(IsActiveDataset() ? UriPath::kActiveGet
-                                                                                    : UriPath::kPendingGet);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(IsActiveDataset() ? kUriActiveGet : kUriPendingGet);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     if (aLength + length > 0)
@@ -646,18 +628,10 @@
 
 ActiveDatasetManager::ActiveDatasetManager(Instance &aInstance)
     : DatasetManager(aInstance, Dataset::kActive, ActiveDatasetManager::HandleTimer)
-    , mResourceGet(UriPath::kActiveGet, &ActiveDatasetManager::HandleGet, this)
-#if OPENTHREAD_FTD
-    , mResourceSet(UriPath::kActiveSet, &ActiveDatasetManager::HandleSet, this)
-#endif
 {
-    Get<Tmf::Agent>().AddResource(mResourceGet);
 }
 
-bool ActiveDatasetManager::IsPartiallyComplete(void) const
-{
-    return mLocal.IsSaved() && !mTimestampValid;
-}
+bool ActiveDatasetManager::IsPartiallyComplete(void) const { return mLocal.IsSaved() && !mTimestampValid; }
 
 bool ActiveDatasetManager::IsCommissioned(void) const
 {
@@ -674,9 +648,9 @@
 }
 
 Error ActiveDatasetManager::Save(const Timestamp &aTimestamp,
-                                 const Message &  aMessage,
+                                 const Message   &aMessage,
                                  uint16_t         aOffset,
-                                 uint8_t          aLength)
+                                 uint16_t         aLength)
 {
     Error   error = kErrorNone;
     Dataset dataset;
@@ -689,30 +663,18 @@
     return error;
 }
 
-void ActiveDatasetManager::HandleGet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<ActiveDatasetManager *>(aContext)->HandleGet(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void ActiveDatasetManager::HandleGet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) const
+template <>
+void ActiveDatasetManager::HandleTmf<kUriActiveGet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     DatasetManager::HandleGet(aMessage, aMessageInfo);
 }
 
-void ActiveDatasetManager::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<ActiveDatasetManager>().HandleTimer();
-}
+void ActiveDatasetManager::HandleTimer(Timer &aTimer) { aTimer.Get<ActiveDatasetManager>().HandleTimer(); }
 
 PendingDatasetManager::PendingDatasetManager(Instance &aInstance)
     : DatasetManager(aInstance, Dataset::kPending, PendingDatasetManager::HandleTimer)
-    , mDelayTimer(aInstance, PendingDatasetManager::HandleDelayTimer)
-    , mResourceGet(UriPath::kPendingGet, &PendingDatasetManager::HandleGet, this)
-#if OPENTHREAD_FTD
-    , mResourceSet(UriPath::kPendingSet, &PendingDatasetManager::HandleSet, this)
-#endif
+    , mDelayTimer(aInstance)
 {
-    Get<Tmf::Agent>().AddResource(mResourceGet);
 }
 
 void PendingDatasetManager::Clear(void)
@@ -764,9 +726,9 @@
 }
 
 Error PendingDatasetManager::Save(const Timestamp &aTimestamp,
-                                  const Message &  aMessage,
+                                  const Message   &aMessage,
                                   uint16_t         aOffset,
-                                  uint8_t          aLength)
+                                  uint16_t         aLength)
 {
     Error   error = kErrorNone;
     Dataset dataset;
@@ -800,15 +762,10 @@
         }
 
         mDelayTimer.StartAt(dataset.GetUpdateTime(), delay);
-        LogInfo("delay timer started %d", delay);
+        LogInfo("delay timer started %lu", ToUlong(delay));
     }
 }
 
-void PendingDatasetManager::HandleDelayTimer(Timer &aTimer)
-{
-    aTimer.Get<PendingDatasetManager>().HandleDelayTimer();
-}
-
 void PendingDatasetManager::HandleDelayTimer(void)
 {
     DelayTimerTlv *delayTimer;
@@ -842,20 +799,13 @@
     return;
 }
 
-void PendingDatasetManager::HandleGet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<PendingDatasetManager *>(aContext)->HandleGet(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void PendingDatasetManager::HandleGet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) const
+template <>
+void PendingDatasetManager::HandleTmf<kUriPendingGet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     DatasetManager::HandleGet(aMessage, aMessageInfo);
 }
 
-void PendingDatasetManager::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<PendingDatasetManager>().HandleTimer();
-}
+void PendingDatasetManager::HandleTimer(Timer &aTimer) { aTimer.Get<PendingDatasetManager>().HandleTimer(); }
 
 } // namespace MeshCoP
 } // namespace ot
diff --git a/src/core/meshcop/dataset_manager.hpp b/src/core/meshcop/dataset_manager.hpp
index 9d54874..d6e1bbf 100644
--- a/src/core/meshcop/dataset_manager.hpp
+++ b/src/core/meshcop/dataset_manager.hpp
@@ -37,7 +37,7 @@
 
 #include "openthread-core-config.h"
 
-#include "coap/coap.hpp"
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "common/timer.hpp"
@@ -45,6 +45,7 @@
 #include "meshcop/dataset.hpp"
 #include "meshcop/dataset_local.hpp"
 #include "net/udp6.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -145,11 +146,11 @@
      * @retval kErrorBusy    A previous request is ongoing.
      *
      */
-    Error SendSetRequest(const Dataset::Info &    aDatasetInfo,
-                         const uint8_t *          aTlvs,
+    Error SendSetRequest(const Dataset::Info     &aDatasetInfo,
+                         const uint8_t           *aTlvs,
                          uint8_t                  aLength,
                          otDatasetMgmtSetCallback aCallback,
-                         void *                   aContext);
+                         void                    *aContext);
 
     /**
      * This method sends a MGMT_GET request.
@@ -164,9 +165,9 @@
      *
      */
     Error SendGetRequest(const Dataset::Components &aDatasetComponents,
-                         const uint8_t *            aTlvTypes,
+                         const uint8_t             *aTlvTypes,
                          uint8_t                    aLength,
-                         const otIp6Address *       aAddress) const;
+                         const otIp6Address        *aAddress) const;
 #if OPENTHREAD_FTD
     /**
      * This method appends the MLE Dataset TLV but excluding MeshCoP Sub Timestamp TLV.
@@ -332,8 +333,8 @@
     bool         mTimestampValid : 1;
 
 private:
-    static void HandleMgmtSetResponse(void *               aContext,
-                                      otMessage *          aMessage,
+    static void HandleMgmtSetResponse(void                *aContext,
+                                      otMessage           *aMessage,
                                       const otMessageInfo *aMessageInfo,
                                       Error                aError);
     void        HandleMgmtSetResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aError);
@@ -344,9 +345,9 @@
     void  HandleDatasetUpdated(void);
     Error AppendDatasetToMessage(const Dataset::Info &aDatasetInfo, Message &aMessage) const;
     void  SendSet(void);
-    void  SendGetResponse(const Coap::Message &   aRequest,
+    void  SendGetResponse(const Coap::Message    &aRequest,
                           const Ip6::MessageInfo &aMessageInfo,
-                          uint8_t *               aTlvs,
+                          uint8_t                *aTlvs,
                           uint8_t                 aLength) const;
 
 #if OPENTHREAD_FTD
@@ -359,12 +360,13 @@
     bool       mMgmtPending : 1;
     TimerMilli mTimer;
 
-    otDatasetMgmtSetCallback mMgmtSetCallback;
-    void *                   mMgmtSetCallbackContext;
+    Callback<otDatasetMgmtSetCallback> mMgmtSetCallback;
 };
 
 class ActiveDatasetManager : public DatasetManager, private NonCopyable
 {
+    friend class Tmf::Agent;
+
 public:
     /**
      * This constructor initializes the ActiveDatasetManager object.
@@ -426,7 +428,7 @@
      * @retval kErrorParse    Could not parse the Dataset from @p aMessage.
      *
      */
-    Error Save(const Timestamp &aTimestamp, const Message &aMessage, uint16_t aOffset, uint8_t aLength);
+    Error Save(const Timestamp &aTimestamp, const Message &aMessage, uint16_t aOffset, uint16_t aLength);
 
     /**
      * This method sets the Operational Dataset in non-volatile memory.
@@ -470,12 +472,6 @@
     void StartLeader(void);
 
     /**
-     * This method stops the Leader functions for maintaining the Active Operational Dataset.
-     *
-     */
-    void StopLeader(void);
-
-    /**
      * This method generate a default Active Operational Dataset.
      *
      * @retval kErrorNone          Successfully generated an Active Operational Dataset.
@@ -487,26 +483,21 @@
 #endif
 
 private:
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+
     static void HandleTimer(Timer &aTimer);
     void        HandleTimer(void) { DatasetManager::HandleTimer(); }
-
-    static void HandleGet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleGet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) const;
-
-#if OPENTHREAD_FTD
-    static void HandleSet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleSet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-#endif
-
-    Coap::Resource mResourceGet;
-
-#if OPENTHREAD_FTD
-    Coap::Resource mResourceSet;
-#endif
 };
 
+DeclareTmfHandler(ActiveDatasetManager, kUriActiveGet);
+#if OPENTHREAD_FTD
+DeclareTmfHandler(ActiveDatasetManager, kUriActiveSet);
+#endif
+
 class PendingDatasetManager : public DatasetManager, private NonCopyable
 {
+    friend class Tmf::Agent;
+
 public:
     /**
      * This constructor initializes the PendingDatasetManager object.
@@ -571,7 +562,7 @@
      * @param[in]  aLength     The length of the Operational Dataset.
      *
      */
-    Error Save(const Timestamp &aTimestamp, const Message &aMessage, uint16_t aOffset, uint8_t aLength);
+    Error Save(const Timestamp &aTimestamp, const Message &aMessage, uint16_t aOffset, uint16_t aLength);
 
     /**
      * This method saves the Operational Dataset in non-volatile memory.
@@ -592,12 +583,6 @@
     void StartLeader(void);
 
     /**
-     * This method stops the Leader functions for maintaining the Active Operational Dataset.
-     *
-     */
-    void StopLeader(void);
-
-    /**
      * This method generates a Pending Dataset from an Active Dataset.
      *
      * @param[in]  aTimestamp  The Active Dataset Timestamp.
@@ -613,26 +598,19 @@
     static void HandleTimer(Timer &aTimer);
     void        HandleTimer(void) { DatasetManager::HandleTimer(); }
 
-    static void HandleDelayTimer(Timer &aTimer);
-    void        HandleDelayTimer(void);
+    void                     HandleDelayTimer(void);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static void HandleGet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleGet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo) const;
+    using DelayTimer = TimerMilliIn<PendingDatasetManager, &PendingDatasetManager::HandleDelayTimer>;
 
-#if OPENTHREAD_FTD
-    static void HandleSet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleSet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-#endif
-
-    TimerMilli mDelayTimer;
-
-    Coap::Resource mResourceGet;
-
-#if OPENTHREAD_FTD
-    Coap::Resource mResourceSet;
-#endif
+    DelayTimer mDelayTimer;
 };
 
+DeclareTmfHandler(PendingDatasetManager, kUriPendingGet);
+#if OPENTHREAD_FTD
+DeclareTmfHandler(PendingDatasetManager, kUriPendingSet);
+#endif
+
 } // namespace MeshCoP
 } // namespace ot
 
diff --git a/src/core/meshcop/dataset_manager_ftd.cpp b/src/core/meshcop/dataset_manager_ftd.cpp
index 4face84..32644e6 100644
--- a/src/core/meshcop/dataset_manager_ftd.cpp
+++ b/src/core/meshcop/dataset_manager_ftd.cpp
@@ -263,7 +263,7 @@
     return (state == StateTlv::kAccept) ? kErrorNone : kErrorDrop;
 }
 
-void DatasetManager::SendSetResponse(const Coap::Message &   aRequest,
+void DatasetManager::SendSetResponse(const Coap::Message    &aRequest,
                                      const Ip6::MessageInfo &aMessageInfo,
                                      StateTlv::State         aState)
 {
@@ -394,24 +394,12 @@
     return error;
 }
 
-void ActiveDatasetManager::StartLeader(void)
-{
-    IgnoreError(GenerateLocal());
-    Get<Tmf::Agent>().AddResource(mResourceSet);
-}
+void ActiveDatasetManager::StartLeader(void) { IgnoreError(GenerateLocal()); }
 
-void ActiveDatasetManager::StopLeader(void)
+template <>
+void ActiveDatasetManager::HandleTmf<kUriActiveSet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    Get<Tmf::Agent>().RemoveResource(mResourceSet);
-}
-
-void ActiveDatasetManager::HandleSet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<ActiveDatasetManager *>(aContext)->HandleSet(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void ActiveDatasetManager::HandleSet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
-{
+    VerifyOrExit(Get<Mle::Mle>().IsLeader());
     SuccessOrExit(DatasetManager::HandleSet(aMessage, aMessageInfo));
     IgnoreError(ApplyConfiguration());
 
@@ -419,24 +407,12 @@
     return;
 }
 
-void PendingDatasetManager::StartLeader(void)
-{
-    StartDelayTimer();
-    Get<Tmf::Agent>().AddResource(mResourceSet);
-}
+void PendingDatasetManager::StartLeader(void) { StartDelayTimer(); }
 
-void PendingDatasetManager::StopLeader(void)
+template <>
+void PendingDatasetManager::HandleTmf<kUriPendingSet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    Get<Tmf::Agent>().RemoveResource(mResourceSet);
-}
-
-void PendingDatasetManager::HandleSet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<PendingDatasetManager *>(aContext)->HandleSet(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void PendingDatasetManager::HandleSet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
-{
+    VerifyOrExit(Get<Mle::Mle>().IsLeader());
     SuccessOrExit(DatasetManager::HandleSet(aMessage, aMessageInfo));
     StartDelayTimer();
 
diff --git a/src/core/meshcop/dataset_updater.cpp b/src/core/meshcop/dataset_updater.cpp
index 36d63d7..114bae5 100644
--- a/src/core/meshcop/dataset_updater.cpp
+++ b/src/core/meshcop/dataset_updater.cpp
@@ -48,14 +48,12 @@
 
 DatasetUpdater::DatasetUpdater(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mCallback(nullptr)
-    , mCallbackContext(nullptr)
-    , mTimer(aInstance, DatasetUpdater::HandleTimer)
+    , mTimer(aInstance)
     , mDataset(nullptr)
 {
 }
 
-Error DatasetUpdater::RequestUpdate(const Dataset::Info &aDataset, Callback aCallback, void *aContext)
+Error DatasetUpdater::RequestUpdate(const Dataset::Info &aDataset, UpdaterCallback aCallback, void *aContext)
 {
     Error    error   = kErrorNone;
     Message *message = nullptr;
@@ -71,9 +69,8 @@
 
     SuccessOrExit(error = message->Append(aDataset));
 
-    mCallback        = aCallback;
-    mCallbackContext = aContext;
-    mDataset         = message;
+    mCallback.Set(aCallback, aContext);
+    mDataset = message;
 
     mTimer.Start(1);
 
@@ -94,15 +91,7 @@
     return;
 }
 
-void DatasetUpdater::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<DatasetUpdater>().HandleTimer();
-}
-
-void DatasetUpdater::HandleTimer(void)
-{
-    PreparePendingDataset();
-}
+void DatasetUpdater::HandleTimer(void) { PreparePendingDataset(); }
 
 void DatasetUpdater::PreparePendingDataset(void)
 {
@@ -171,10 +160,7 @@
     FreeMessage(mDataset);
     mDataset = nullptr;
 
-    if (mCallback != nullptr)
-    {
-        mCallback(aError, mCallbackContext);
-    }
+    mCallback.InvokeIfSet(aError);
 }
 
 void DatasetUpdater::HandleNotifierEvents(Events aEvents)
diff --git a/src/core/meshcop/dataset_updater.hpp b/src/core/meshcop/dataset_updater.hpp
index b3b94c0..ecc998d 100644
--- a/src/core/meshcop/dataset_updater.hpp
+++ b/src/core/meshcop/dataset_updater.hpp
@@ -40,6 +40,7 @@
 
 #include <openthread/dataset_updater.h>
 
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/message.hpp"
 #include "common/non_copyable.hpp"
@@ -72,10 +73,10 @@
      * This type represents the callback function pointer which is called when a Dataset update request finishes,
      * reporting success or failure status of the request.
      *
-     * The function pointer has the syntax `void (*Callback)(Error aError, void *aContext)`.
+     * The function pointer has the syntax `void (*UpdaterCallback)(Error aError, void *aContext)`.
      *
      */
-    typedef otDatasetUpdaterCallback Callback;
+    typedef otDatasetUpdaterCallback UpdaterCallback;
 
     /**
      * This method requests an update to Operational Dataset.
@@ -94,7 +95,7 @@
      * @retval kErrorNoBufs         Could not allocated buffer to save Dataset.
      *
      */
-    Error RequestUpdate(const Dataset::Info &aDataset, Callback aCallback, void *aContext);
+    Error RequestUpdate(const Dataset::Info &aDataset, UpdaterCallback aCallback, void *aContext);
 
     /**
      * This method cancels an ongoing (if any) Operational Dataset update request.
@@ -118,16 +119,16 @@
     // Retry interval (in ms) when preparing and/or sending Pending Dataset fails.
     static constexpr uint32_t kRetryInterval = 1000;
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
-    void        PreparePendingDataset(void);
-    void        Finish(Error aError);
-    void        HandleNotifierEvents(Events aEvents);
+    void HandleTimer(void);
+    void PreparePendingDataset(void);
+    void Finish(Error aError);
+    void HandleNotifierEvents(Events aEvents);
 
-    Callback   mCallback;
-    void *     mCallbackContext;
-    TimerMilli mTimer;
-    Message *  mDataset;
+    using UpdaterTimer = TimerMilliIn<DatasetUpdater, &DatasetUpdater::HandleTimer>;
+
+    Callback<UpdaterCallback> mCallback;
+    UpdaterTimer              mTimer;
+    Message                  *mDataset;
 };
 
 } // namespace MeshCoP
diff --git a/src/core/meshcop/dtls.cpp b/src/core/meshcop/dtls.cpp
index 7a558aa..28337a6 100644
--- a/src/core/meshcop/dtls.cpp
+++ b/src/core/meshcop/dtls.cpp
@@ -66,8 +66,12 @@
 #endif
 
 #if defined(MBEDTLS_KEY_EXCHANGE__WITH_CERT__ENABLED) || defined(MBEDTLS_KEY_EXCHANGE_WITH_CERT_ENABLED)
+#if (MBEDTLS_VERSION_NUMBER >= 0x03020000)
+const uint16_t Dtls::sSignatures[] = {MBEDTLS_TLS1_3_SIG_ECDSA_SECP256R1_SHA256, MBEDTLS_TLS1_3_SIG_NONE};
+#else
 const int Dtls::sHashes[] = {MBEDTLS_MD_SHA256, MBEDTLS_MD_NONE};
 #endif
+#endif
 
 Dtls::Dtls(Instance &aInstance, bool aLayerTwoSecurity)
     : InstanceLocator(aInstance)
@@ -79,12 +83,7 @@
     , mTimerSet(false)
     , mLayerTwoSecurity(aLayerTwoSecurity)
     , mReceiveMessage(nullptr)
-    , mConnectedHandler(nullptr)
-    , mReceiveHandler(nullptr)
-    , mContext(nullptr)
     , mSocket(aInstance)
-    , mTransportCallback(nullptr)
-    , mTransportContext(nullptr)
     , mMessageSubType(Message::kSubTypeNone)
     , mMessageDefaultSubType(Message::kSubTypeNone)
 {
@@ -143,10 +142,9 @@
 
     SuccessOrExit(error = mSocket.Open(&Dtls::HandleUdpReceive, this));
 
-    mReceiveHandler   = aReceiveHandler;
-    mConnectedHandler = aConnectedHandler;
-    mContext          = aContext;
-    mState            = kStateOpen;
+    mConnectedCallback.Set(aConnectedHandler, aContext);
+    mReceiveCallback.Set(aReceiveHandler, aContext);
+    mState = kStateOpen;
 
 exit:
     return error;
@@ -180,17 +178,11 @@
         ExitNow();
 
     case Dtls::kStateOpen:
-        IgnoreError(mSocket.Connect(Ip6::SockAddr(aMessageInfo.GetPeerAddr(), aMessageInfo.GetPeerPort())));
-
         mMessageInfo.SetPeerAddr(aMessageInfo.GetPeerAddr());
         mMessageInfo.SetPeerPort(aMessageInfo.GetPeerPort());
         mMessageInfo.SetIsHostInterface(aMessageInfo.IsHostInterface());
 
-        if (Get<ThreadNetif>().HasUnicastAddress(aMessageInfo.GetSockAddr()))
-        {
-            mMessageInfo.SetSockAddr(aMessageInfo.GetSockAddr());
-        }
-
+        mMessageInfo.SetSockAddr(aMessageInfo.GetSockAddr());
         mMessageInfo.SetSockPort(aMessageInfo.GetSockPort());
 
         SuccessOrExit(Setup(false));
@@ -216,19 +208,16 @@
     return;
 }
 
-uint16_t Dtls::GetUdpPort(void) const
-{
-    return mSocket.GetSockName().GetPort();
-}
+uint16_t Dtls::GetUdpPort(void) const { return mSocket.GetSockName().GetPort(); }
 
 Error Dtls::Bind(uint16_t aPort)
 {
     Error error;
 
     VerifyOrExit(mState == kStateOpen, error = kErrorInvalidState);
-    VerifyOrExit(mTransportCallback == nullptr, error = kErrorAlready);
+    VerifyOrExit(!mTransportCallback.IsSet(), error = kErrorAlready);
 
-    SuccessOrExit(error = mSocket.Bind(aPort, OT_NETIF_UNSPECIFIED));
+    SuccessOrExit(error = mSocket.Bind(aPort, Ip6::kNetifUnspecified));
 
 exit:
     return error;
@@ -240,10 +229,9 @@
 
     VerifyOrExit(mState == kStateOpen, error = kErrorInvalidState);
     VerifyOrExit(!mSocket.IsBound(), error = kErrorAlready);
-    VerifyOrExit(mTransportCallback == nullptr, error = kErrorAlready);
+    VerifyOrExit(!mTransportCallback.IsSet(), error = kErrorAlready);
 
-    mTransportCallback = aCallback;
-    mTransportContext  = aContext;
+    mTransportCallback.Set(aCallback, aContext);
 
 exit:
     return error;
@@ -289,8 +277,13 @@
 #endif
 
     mbedtls_ssl_conf_rng(&mConf, Crypto::MbedTls::CryptoSecurePrng, nullptr);
+#if (MBEDTLS_VERSION_NUMBER >= 0x03020000)
+    mbedtls_ssl_conf_min_tls_version(&mConf, MBEDTLS_SSL_VERSION_TLS1_2);
+    mbedtls_ssl_conf_max_tls_version(&mConf, MBEDTLS_SSL_VERSION_TLS1_2);
+#else
     mbedtls_ssl_conf_min_version(&mConf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
     mbedtls_ssl_conf_max_version(&mConf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
+#endif
 
     OT_ASSERT(mCipherSuites[1] == 0);
     mbedtls_ssl_conf_ciphersuites(&mConf, mCipherSuites);
@@ -302,8 +295,12 @@
         mbedtls_ssl_conf_curves(&mConf, sCurves);
 #endif
 #if defined(MBEDTLS_KEY_EXCHANGE__WITH_CERT__ENABLED) || defined(MBEDTLS_KEY_EXCHANGE_WITH_CERT_ENABLED)
+#if (MBEDTLS_VERSION_NUMBER >= 0x03020000)
+        mbedtls_ssl_conf_sig_algs(&mConf, sSignatures);
+#else
         mbedtls_ssl_conf_sig_hashes(&mConf, sHashes);
 #endif
+#endif
     }
 
 #if (MBEDTLS_VERSION_NUMBER >= 0x03000000)
@@ -435,10 +432,9 @@
 {
     Disconnect();
 
-    mState             = kStateClosed;
-    mTransportCallback = nullptr;
-    mTransportContext  = nullptr;
-    mTimerSet          = false;
+    mState    = kStateClosed;
+    mTimerSet = false;
+    mTransportCallback.Clear();
 
     IgnoreError(mSocket.Close());
     mTimer.Stop();
@@ -676,10 +672,7 @@
     return rval;
 }
 
-int Dtls::HandleMbedtlsGetTimer(void *aContext)
-{
-    return static_cast<Dtls *>(aContext)->HandleMbedtlsGetTimer();
-}
+int Dtls::HandleMbedtlsGetTimer(void *aContext) { return static_cast<Dtls *>(aContext)->HandleMbedtlsGetTimer(); }
 
 int Dtls::HandleMbedtlsGetTimer(void)
 {
@@ -749,9 +742,9 @@
 
 #if (MBEDTLS_VERSION_NUMBER >= 0x03000000)
 
-void Dtls::HandleMbedtlsExportKeys(void *                      aContext,
+void Dtls::HandleMbedtlsExportKeys(void                       *aContext,
                                    mbedtls_ssl_key_export_type aType,
-                                   const unsigned char *       aMasterSecret,
+                                   const unsigned char        *aMasterSecret,
                                    size_t                      aMasterSecretLen,
                                    const unsigned char         aClientRandom[32],
                                    const unsigned char         aServerRandom[32],
@@ -762,7 +755,7 @@
 }
 
 void Dtls::HandleMbedtlsExportKeys(mbedtls_ssl_key_export_type aType,
-                                   const unsigned char *       aMasterSecret,
+                                   const unsigned char        *aMasterSecret,
                                    size_t                      aMasterSecretLen,
                                    const unsigned char         aClientRandom[32],
                                    const unsigned char         aServerRandom[32],
@@ -796,7 +789,7 @@
 
 #else
 
-int Dtls::HandleMbedtlsExportKeys(void *               aContext,
+int Dtls::HandleMbedtlsExportKeys(void                *aContext,
                                   const unsigned char *aMasterSecret,
                                   const unsigned char *aKeyBlock,
                                   size_t               aMacLength,
@@ -850,11 +843,7 @@
     case kStateCloseNotify:
         mState = kStateOpen;
         mTimer.Stop();
-
-        if (mConnectedHandler != nullptr)
-        {
-            mConnectedHandler(mContext, false);
-        }
+        mConnectedCallback.InvokeIfSet(false);
         break;
 
     default:
@@ -878,11 +867,7 @@
             if (mSsl.MBEDTLS_PRIVATE(state) == MBEDTLS_SSL_HANDSHAKE_OVER)
             {
                 mState = kStateConnected;
-
-                if (mConnectedHandler != nullptr)
-                {
-                    mConnectedHandler(mContext, true);
-                }
+                mConnectedCallback.InvokeIfSet(true);
             }
         }
         else
@@ -892,10 +877,7 @@
 
         if (rval > 0)
         {
-            if (mReceiveHandler != nullptr)
-            {
-                mReceiveHandler(mContext, buf, static_cast<uint16_t>(rval));
-            }
+            mReceiveCallback.InvokeIfSet(buf, static_cast<uint16_t>(rval));
         }
         else if (rval == 0 || rval == MBEDTLS_ERR_SSL_WANT_READ || rval == MBEDTLS_ERR_SSL_WANT_WRITE)
         {
@@ -970,20 +952,20 @@
     switch (aLevel)
     {
     case 1:
-        LogCrit("[%hu] %s", mSocket.GetSockName().mPort, aStr);
+        LogCrit("[%u] %s", mSocket.GetSockName().mPort, aStr);
         break;
 
     case 2:
-        LogWarn("[%hu] %s", mSocket.GetSockName().mPort, aStr);
+        LogWarn("[%u] %s", mSocket.GetSockName().mPort, aStr);
         break;
 
     case 3:
-        LogInfo("[%hu] %s", mSocket.GetSockName().mPort, aStr);
+        LogInfo("[%u] %s", mSocket.GetSockName().mPort, aStr);
         break;
 
     case 4:
     default:
-        LogDebg("[%hu] %s", mSocket.GetSockName().mPort, aStr);
+        LogDebg("[%u] %s", mSocket.GetSockName().mPort, aStr);
         break;
     }
 }
@@ -993,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);
 
@@ -1005,9 +987,9 @@
         message->SetSubType(aMessageSubType);
     }
 
-    if (mTransportCallback)
+    if (mTransportCallback.IsSet())
     {
-        SuccessOrExit(error = mTransportCallback(mTransportContext, *message, mMessageInfo));
+        SuccessOrExit(error = mTransportCallback.Invoke(*message, mMessageInfo));
     }
     else
     {
diff --git a/src/core/meshcop/dtls.hpp b/src/core/meshcop/dtls.hpp
index 0f809de..129ed3c 100644
--- a/src/core/meshcop/dtls.hpp
+++ b/src/core/meshcop/dtls.hpp
@@ -51,6 +51,7 @@
 #endif
 #endif
 
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/message.hpp"
 #include "common/random.hpp"
@@ -394,16 +395,16 @@
 #ifdef MBEDTLS_SSL_EXPORT_KEYS
 #if (MBEDTLS_VERSION_NUMBER >= 0x03000000)
 
-    static void HandleMbedtlsExportKeys(void *                      aContext,
+    static void HandleMbedtlsExportKeys(void                       *aContext,
                                         mbedtls_ssl_key_export_type aType,
-                                        const unsigned char *       aMasterSecret,
+                                        const unsigned char        *aMasterSecret,
                                         size_t                      aMasterSecretLen,
                                         const unsigned char         aClientRandom[32],
                                         const unsigned char         aServerRandom[32],
                                         mbedtls_tls_prf_types       aTlsPrfType);
 
     void HandleMbedtlsExportKeys(mbedtls_ssl_key_export_type aType,
-                                 const unsigned char *       aMasterSecret,
+                                 const unsigned char        *aMasterSecret,
                                  size_t                      aMasterSecretLen,
                                  const unsigned char         aClientRandom[32],
                                  const unsigned char         aServerRandom[32],
@@ -411,17 +412,17 @@
 
 #else
 
-    static int HandleMbedtlsExportKeys(void *               aContext,
-                                       const unsigned char *aMasterSecret,
-                                       const unsigned char *aKeyBlock,
-                                       size_t               aMacLength,
-                                       size_t               aKeyLength,
-                                       size_t               aIvLength);
-    int        HandleMbedtlsExportKeys(const unsigned char *aMasterSecret,
-                                       const unsigned char *aKeyBlock,
-                                       size_t               aMacLength,
-                                       size_t               aKeyLength,
-                                       size_t               aIvLength);
+    static int       HandleMbedtlsExportKeys(void                *aContext,
+                                             const unsigned char *aMasterSecret,
+                                             const unsigned char *aKeyBlock,
+                                             size_t               aMacLength,
+                                             size_t               aKeyLength,
+                                             size_t               aIvLength);
+    int              HandleMbedtlsExportKeys(const unsigned char *aMasterSecret,
+                                             const unsigned char *aKeyBlock,
+                                             size_t               aMacLength,
+                                             size_t               aKeyLength,
+                                             size_t               aIvLength);
 
 #endif // (MBEDTLS_VERSION_NUMBER >= 0x03000000)
 #endif // MBEDTLS_SSL_EXPORT_KEYS
@@ -449,16 +450,20 @@
 #endif
 
 #if defined(MBEDTLS_KEY_EXCHANGE__WITH_CERT__ENABLED) || defined(MBEDTLS_KEY_EXCHANGE_WITH_CERT_ENABLED)
+#if (MBEDTLS_VERSION_NUMBER >= 0x03020000)
+    static const uint16_t sSignatures[];
+#else
     static const int sHashes[];
 #endif
+#endif
 
 #if OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE
 #ifdef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
-    const uint8_t *    mCaChainSrc;
+    const uint8_t     *mCaChainSrc;
     uint32_t           mCaChainLength;
-    const uint8_t *    mOwnCertSrc;
+    const uint8_t     *mOwnCertSrc;
     uint32_t           mOwnCertLength;
-    const uint8_t *    mPrivateKeySrc;
+    const uint8_t     *mPrivateKeySrc;
     uint32_t           mPrivateKeyLength;
     mbedtls_x509_crt   mCaChain;
     mbedtls_x509_crt   mOwnCert;
@@ -490,15 +495,13 @@
 
     Message *mReceiveMessage;
 
-    ConnectedHandler mConnectedHandler;
-    ReceiveHandler   mReceiveHandler;
-    void *           mContext;
+    Callback<ConnectedHandler> mConnectedCallback;
+    Callback<ReceiveHandler>   mReceiveCallback;
 
     Ip6::MessageInfo mMessageInfo;
     Ip6::Udp::Socket mSocket;
 
-    TransportCallback mTransportCallback;
-    void *            mTransportContext;
+    Callback<TransportCallback> mTransportCallback;
 
     Message::SubType mMessageSubType;
     Message::SubType mMessageDefaultSubType;
diff --git a/src/core/meshcop/energy_scan_client.cpp b/src/core/meshcop/energy_scan_client.cpp
index 09e8fbd..905d8a6 100644
--- a/src/core/meshcop/energy_scan_client.cpp
+++ b/src/core/meshcop/energy_scan_client.cpp
@@ -43,6 +43,7 @@
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
+#include "common/num_utils.hpp"
 #include "meshcop/meshcop.hpp"
 #include "meshcop/meshcop_tlvs.hpp"
 #include "thread/thread_netif.hpp"
@@ -54,30 +55,26 @@
 
 EnergyScanClient::EnergyScanClient(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mCallback(nullptr)
-    , mContext(nullptr)
-    , mEnergyScan(UriPath::kEnergyReport, &EnergyScanClient::HandleReport, this)
 {
-    Get<Tmf::Agent>().AddResource(mEnergyScan);
 }
 
 Error EnergyScanClient::SendQuery(uint32_t                           aChannelMask,
                                   uint8_t                            aCount,
                                   uint16_t                           aPeriod,
                                   uint16_t                           aScanDuration,
-                                  const Ip6::Address &               aAddress,
+                                  const Ip6::Address                &aAddress,
                                   otCommissionerEnergyReportCallback aCallback,
-                                  void *                             aContext)
+                                  void                              *aContext)
 {
     Error                   error = kErrorNone;
     MeshCoP::ChannelMaskTlv channelMask;
     Tmf::MessageInfo        messageInfo(GetInstance());
-    Coap::Message *         message = nullptr;
+    Coap::Message          *message = nullptr;
 
     VerifyOrExit(Get<MeshCoP::Commissioner>().IsActive(), error = kErrorInvalidState);
     VerifyOrExit((message = Get<Tmf::Agent>().NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
 
-    SuccessOrExit(error = message->InitAsPost(aAddress, UriPath::kEnergyScan));
+    SuccessOrExit(error = message->InitAsPost(aAddress, kUriEnergyScan));
     SuccessOrExit(error = message->SetPayloadMarker());
 
     SuccessOrExit(
@@ -94,49 +91,34 @@
     messageInfo.SetSockAddrToRlocPeerAddrTo(aAddress);
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("sent query");
+    LogInfo("Sent %s", UriToString<kUriEnergyScan>());
 
-    mCallback = aCallback;
-    mContext  = aContext;
+    mCallback.Set(aCallback, aContext);
 
 exit:
     FreeMessageOnError(message, error);
     return error;
 }
 
-void EnergyScanClient::HandleReport(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
+template <>
+void EnergyScanClient::HandleTmf<kUriEnergyReport>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    static_cast<EnergyScanClient *>(aContext)->HandleReport(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void EnergyScanClient::HandleReport(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
-{
-    uint32_t mask;
-
-    OT_TOOL_PACKED_BEGIN
-    struct
-    {
-        MeshCoP::EnergyListTlv tlv;
-        uint8_t                list[OPENTHREAD_CONFIG_TMF_ENERGY_SCAN_MAX_RESULTS];
-    } OT_TOOL_PACKED_END energyList;
+    uint32_t               mask;
+    MeshCoP::EnergyListTlv energyListTlv;
 
     VerifyOrExit(aMessage.IsConfirmablePostRequest());
 
-    LogInfo("received report");
+    LogInfo("Received %s", UriToString<kUriEnergyReport>());
 
     VerifyOrExit((mask = MeshCoP::ChannelMaskTlv::GetChannelMask(aMessage)) != 0);
 
-    SuccessOrExit(MeshCoP::Tlv::FindTlv(aMessage, MeshCoP::Tlv::kEnergyList, sizeof(energyList), energyList.tlv));
-    VerifyOrExit(energyList.tlv.IsValid());
+    SuccessOrExit(MeshCoP::Tlv::FindTlv(aMessage, MeshCoP::Tlv::kEnergyList, sizeof(energyListTlv), energyListTlv));
 
-    if (mCallback != nullptr)
-    {
-        mCallback(mask, energyList.list, energyList.tlv.GetLength(), mContext);
-    }
+    mCallback.InvokeIfSet(mask, energyListTlv.GetEnergyList(), energyListTlv.GetEnergyListLength());
 
     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/energy_scan_client.hpp b/src/core/meshcop/energy_scan_client.hpp
index 238344c..97d9873 100644
--- a/src/core/meshcop/energy_scan_client.hpp
+++ b/src/core/meshcop/energy_scan_client.hpp
@@ -41,9 +41,11 @@
 #include <openthread/commissioner.h>
 
 #include "coap/coap.hpp"
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "net/ip6_address.hpp"
 #include "net/udp6.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -53,6 +55,8 @@
  */
 class EnergyScanClient : public InstanceLocator
 {
+    friend class Tmf::Agent;
+
 public:
     /**
      * This constructor initializes the object.
@@ -79,20 +83,18 @@
                     uint8_t                            aCount,
                     uint16_t                           aPeriod,
                     uint16_t                           aScanDuration,
-                    const Ip6::Address &               aAddress,
+                    const Ip6::Address                &aAddress,
                     otCommissionerEnergyReportCallback aCallback,
-                    void *                             aContext);
+                    void                              *aContext);
 
 private:
-    static void HandleReport(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleReport(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    otCommissionerEnergyReportCallback mCallback;
-    void *                             mContext;
-
-    Coap::Resource mEnergyScan;
+    Callback<otCommissionerEnergyReportCallback> mCallback;
 };
 
+DeclareTmfHandler(EnergyScanClient, kUriEnergyReport);
+
 /**
  * @}
  */
diff --git a/src/core/meshcop/joiner.cpp b/src/core/meshcop/joiner.cpp
index 4c48316..84b7d55 100644
--- a/src/core/meshcop/joiner.cpp
+++ b/src/core/meshcop/joiner.cpp
@@ -62,17 +62,13 @@
     , mId()
     , mDiscerner()
     , mState(kStateIdle)
-    , mCallback(nullptr)
-    , mContext(nullptr)
     , mJoinerRouterIndex(0)
     , mFinalizeMessage(nullptr)
-    , mTimer(aInstance, Joiner::HandleTimer)
-    , mJoinerEntrust(UriPath::kJoinerEntrust, &Joiner::HandleJoinerEntrust, this)
+    , mTimer(aInstance)
 {
     SetIdFromIeeeEui64();
     mDiscerner.Clear();
     memset(mJoinerRouters, 0, sizeof(mJoinerRouters));
-    Get<Tmf::Agent>().AddResource(mJoinerEntrust);
 }
 
 void Joiner::SetIdFromIeeeEui64(void)
@@ -83,10 +79,7 @@
     ComputeJoinerId(eui64, mId);
 }
 
-const JoinerDiscerner *Joiner::GetDiscerner(void) const
-{
-    return mDiscerner.IsEmpty() ? nullptr : &mDiscerner;
-}
+const JoinerDiscerner *Joiner::GetDiscerner(void) const { return mDiscerner.IsEmpty() ? nullptr : &mDiscerner; }
 
 Error Joiner::SetDiscerner(const JoinerDiscerner &aDiscerner)
 {
@@ -128,14 +121,14 @@
     return;
 }
 
-Error Joiner::Start(const char *     aPskd,
-                    const char *     aProvisioningUrl,
-                    const char *     aVendorName,
-                    const char *     aVendorModel,
-                    const char *     aVendorSwVersion,
-                    const char *     aVendorData,
+Error Joiner::Start(const char      *aPskd,
+                    const char      *aProvisioningUrl,
+                    const char      *aVendorName,
+                    const char      *aVendorModel,
+                    const char      *aVendorSwVersion,
+                    const char      *aVendorData,
                     otJoinerCallback aCallback,
-                    void *           aContext)
+                    void            *aContext)
 {
     Error                        error;
     JoinerPskd                   joinerPskd;
@@ -159,8 +152,8 @@
     Get<Mac::Mac>().SetExtAddress(randomAddress);
     Get<Mle::MleRouter>().UpdateLinkLocalAddress();
 
-    SuccessOrExit(error = Get<Coap::CoapSecure>().Start(kJoinerUdpPort));
-    Get<Coap::CoapSecure>().SetPsk(joinerPskd);
+    SuccessOrExit(error = Get<Tmf::SecureAgent>().Start(kJoinerUdpPort));
+    Get<Tmf::SecureAgent>().SetPsk(joinerPskd);
 
     for (JoinerRouter &router : mJoinerRouters)
     {
@@ -182,9 +175,8 @@
     SuccessOrExit(error = Get<Mle::DiscoverScanner>().Discover(Mac::ChannelMask(0), Get<Mac::Mac>().GetPanId(),
                                                                /* aJoiner */ true, /* aEnableFiltering */ true,
                                                                &filterIndexes, HandleDiscoverResult, this));
-    mCallback = aCallback;
-    mContext  = aContext;
 
+    mCallback.Set(aCallback, aContext);
     SetState(kStateDiscover);
 
 exit:
@@ -202,7 +194,7 @@
     LogInfo("Joiner stopped");
 
     // Callback is set to `nullptr` to skip calling it from `Finish()`
-    mCallback = nullptr;
+    mCallback.Clear();
     Finish(kErrorAbort);
 }
 
@@ -217,24 +209,21 @@
     case kStateConnected:
     case kStateEntrust:
     case kStateJoined:
-        Get<Coap::CoapSecure>().Disconnect();
+        Get<Tmf::SecureAgent>().Disconnect();
         IgnoreError(Get<Ip6::Filter>().RemoveUnsecurePort(kJoinerUdpPort));
         mTimer.Stop();
 
         OT_FALL_THROUGH;
 
     case kStateDiscover:
-        Get<Coap::CoapSecure>().Stop();
+        Get<Tmf::SecureAgent>().Stop();
         break;
     }
 
     SetState(kStateIdle);
     FreeJoinerFinalizeMessage();
 
-    if (mCallback)
-    {
-        mCallback(aError, mContext);
-    }
+    mCallback.InvokeIfSet(aError);
 
 exit:
     return;
@@ -244,25 +233,13 @@
 {
     int16_t priority;
 
-    if (aRssi == OT_RADIO_RSSI_INVALID)
+    if (aRssi == Radio::kInvalidRssi)
     {
         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
@@ -396,7 +373,7 @@
 
     sockAddr.GetAddress().SetToLinkLocalAddress(aRouter.mExtAddr);
 
-    SuccessOrExit(error = Get<Coap::CoapSecure>().Connect(sockAddr, Joiner::HandleSecureCoapClientConnect, this));
+    SuccessOrExit(error = Get<Tmf::SecureAgent>().Connect(sockAddr, Joiner::HandleSecureCoapClientConnect, this));
 
     SetState(kStateConnect);
 
@@ -418,7 +395,7 @@
     {
         SetState(kStateConnected);
         SendJoinerFinalize();
-        mTimer.Start(kReponseTimeout);
+        mTimer.Start(kResponseTimeout);
     }
     else
     {
@@ -436,30 +413,18 @@
                                            const char *aVendorData)
 {
     Error                 error = kErrorNone;
-    VendorNameTlv         vendorNameTlv;
-    VendorModelTlv        vendorModelTlv;
-    VendorSwVersionTlv    vendorSwVersionTlv;
     VendorStackVersionTlv vendorStackVersionTlv;
-    ProvisioningUrlTlv    provisioningUrlTlv;
 
-    mFinalizeMessage = Get<Coap::CoapSecure>().NewPriorityConfirmablePostMessage(UriPath::kJoinerFinalize);
+    mFinalizeMessage = Get<Tmf::SecureAgent>().NewPriorityConfirmablePostMessage(kUriJoinerFinalize);
     VerifyOrExit(mFinalizeMessage != nullptr, error = kErrorNoBufs);
 
     mFinalizeMessage->SetOffset(mFinalizeMessage->GetLength());
 
     SuccessOrExit(error = Tlv::Append<StateTlv>(*mFinalizeMessage, StateTlv::kAccept));
 
-    vendorNameTlv.Init();
-    vendorNameTlv.SetVendorName(aVendorName);
-    SuccessOrExit(error = vendorNameTlv.AppendTo(*mFinalizeMessage));
-
-    vendorModelTlv.Init();
-    vendorModelTlv.SetVendorModel(aVendorModel);
-    SuccessOrExit(error = vendorModelTlv.AppendTo(*mFinalizeMessage));
-
-    vendorSwVersionTlv.Init();
-    vendorSwVersionTlv.SetVendorSwVersion(aVendorSwVersion);
-    SuccessOrExit(error = vendorSwVersionTlv.AppendTo(*mFinalizeMessage));
+    SuccessOrExit(error = Tlv::Append<VendorNameTlv>(*mFinalizeMessage, aVendorName));
+    SuccessOrExit(error = Tlv::Append<VendorModelTlv>(*mFinalizeMessage, aVendorModel));
+    SuccessOrExit(error = Tlv::Append<VendorSwVersionTlv>(*mFinalizeMessage, aVendorSwVersion));
 
     vendorStackVersionTlv.Init();
     vendorStackVersionTlv.SetOui(OPENTHREAD_CONFIG_STACK_VENDOR_OUI);
@@ -470,18 +435,12 @@
 
     if (aVendorData != nullptr)
     {
-        VendorDataTlv vendorDataTlv;
-        vendorDataTlv.Init();
-        vendorDataTlv.SetVendorData(aVendorData);
-        SuccessOrExit(error = vendorDataTlv.AppendTo(*mFinalizeMessage));
+        SuccessOrExit(error = Tlv::Append<VendorDataTlv>(*mFinalizeMessage, aVendorData));
     }
 
-    provisioningUrlTlv.Init();
-    provisioningUrlTlv.SetProvisioningUrl(aProvisioningUrl);
-
-    if (provisioningUrlTlv.GetLength() > 0)
+    if (aProvisioningUrl != nullptr)
     {
-        SuccessOrExit(error = provisioningUrlTlv.AppendTo(*mFinalizeMessage));
+        SuccessOrExit(error = Tlv::Append<ProvisioningUrlTlv>(*mFinalizeMessage, aProvisioningUrl));
     }
 
 exit:
@@ -512,17 +471,17 @@
     LogCertMessage("[THCI] direction=send | type=JOIN_FIN.req |", *mFinalizeMessage);
 #endif
 
-    SuccessOrExit(Get<Coap::CoapSecure>().SendMessage(*mFinalizeMessage, Joiner::HandleJoinerFinalizeResponse, this));
+    SuccessOrExit(Get<Tmf::SecureAgent>().SendMessage(*mFinalizeMessage, Joiner::HandleJoinerFinalizeResponse, this));
     mFinalizeMessage = nullptr;
 
-    LogInfo("Joiner sent finalize");
+    LogInfo("Sent %s", UriToString<kUriJoinerFinalize>());
 
 exit:
     return;
 }
 
-void Joiner::HandleJoinerFinalizeResponse(void *               aContext,
-                                          otMessage *          aMessage,
+void Joiner::HandleJoinerFinalizeResponse(void                *aContext,
+                                          otMessage           *aMessage,
                                           const otMessageInfo *aMessageInfo,
                                           Error                aResult)
 {
@@ -544,32 +503,27 @@
     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);
 #endif
 
 exit:
-    Get<Coap::CoapSecure>().Disconnect();
+    Get<Tmf::SecureAgent>().Disconnect();
     IgnoreError(Get<Ip6::Filter>().RemoveUnsecurePort(kJoinerUdpPort));
 }
 
-void Joiner::HandleJoinerEntrust(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<Joiner *>(aContext)->HandleJoinerEntrust(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void Joiner::HandleJoinerEntrust(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void Joiner::HandleTmf<kUriJoinerEntrust>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error         error;
     Dataset::Info datasetInfo;
 
     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();
@@ -595,7 +549,7 @@
 void Joiner::SendJoinerEntrustResponse(const Coap::Message &aRequest, const Ip6::MessageInfo &aRequestInfo)
 {
     Error            error = kErrorNone;
-    Coap::Message *  message;
+    Coap::Message   *message;
     Ip6::MessageInfo responseInfo(aRequestInfo);
 
     message = Get<Tmf::Agent>().NewPriorityResponseMessage(aRequest);
@@ -608,30 +562,19 @@
 
     SetState(kStateJoined);
 
-    LogInfo("Joiner sent entrust response");
+    LogInfo("Sent %s response", UriToString<kUriJoinerEntrust>());
     LogCert("[THCI] direction=send | type=JOIN_ENT.rsp");
 
 exit:
     FreeMessageOnError(message, error);
 }
 
-void Joiner::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Joiner>().HandleTimer();
-}
-
 void Joiner::HandleTimer(void)
 {
     Error error = kErrorNone;
 
     switch (mState)
     {
-    case kStateIdle:
-    case kStateDiscover:
-    case kStateConnect:
-        OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
-
     case kStateConnected:
     case kStateEntrust:
         error = kErrorResponseTimeout;
@@ -646,6 +589,11 @@
 
         error = kErrorNone;
         break;
+
+    case kStateIdle:
+    case kStateDiscover:
+    case kStateConnect:
+        OT_ASSERT(false);
     }
 
     Finish(error);
diff --git a/src/core/meshcop/joiner.hpp b/src/core/meshcop/joiner.hpp
index ac894e9..81e4b2b 100644
--- a/src/core/meshcop/joiner.hpp
+++ b/src/core/meshcop/joiner.hpp
@@ -40,10 +40,10 @@
 
 #include <openthread/joiner.h>
 
-#include "coap/coap.hpp"
 #include "coap/coap_message.hpp"
 #include "coap/coap_secure.hpp"
 #include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/log.hpp"
 #include "common/message.hpp"
@@ -53,6 +53,7 @@
 #include "meshcop/meshcop.hpp"
 #include "meshcop/meshcop_tlvs.hpp"
 #include "thread/discover_scanner.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -60,6 +61,8 @@
 
 class Joiner : public InstanceLocator, private NonCopyable
 {
+    friend class Tmf::Agent;
+
 public:
     /**
      * This enumeration type defines the Joiner State.
@@ -100,14 +103,14 @@
      * @retval kErrorInvalidState  The IPv6 stack is not enabled or Thread stack is fully enabled.
      *
      */
-    Error Start(const char *     aPskd,
-                const char *     aProvisioningUrl,
-                const char *     aVendorName,
-                const char *     aVendorModel,
-                const char *     aVendorSwVersion,
-                const char *     aVendorData,
+    Error Start(const char      *aPskd,
+                const char      *aProvisioningUrl,
+                const char      *aVendorName,
+                const char      *aVendorModel,
+                const char      *aVendorSwVersion,
+                const char      *aVendorData,
                 otJoinerCallback aCallback,
-                void *           aContext);
+                void            *aContext);
 
     /**
      * This method stops the Joiner service.
@@ -182,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
     {
@@ -199,17 +202,15 @@
     static void HandleSecureCoapClientConnect(bool aConnected, void *aContext);
     void        HandleSecureCoapClientConnect(bool aConnected);
 
-    static void HandleJoinerFinalizeResponse(void *               aContext,
-                                             otMessage *          aMessage,
+    static void HandleJoinerFinalizeResponse(void                *aContext,
+                                             otMessage           *aMessage,
                                              const otMessageInfo *aMessageInfo,
                                              Error                aResult);
     void HandleJoinerFinalizeResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult);
 
-    static void HandleJoinerEntrust(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleJoinerEntrust(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTimer(void);
 
     void    SetState(State aState);
     void    SetIdFromIeeeEui64(void);
@@ -232,23 +233,25 @@
     void LogCertMessage(const char *aText, const Coap::Message &aMessage) const;
 #endif
 
+    using JoinerTimer = TimerMilliIn<Joiner, &Joiner::HandleTimer>;
+
     Mac::ExtAddress mId;
     JoinerDiscerner mDiscerner;
 
     State mState;
 
-    otJoinerCallback mCallback;
-    void *           mContext;
+    Callback<otJoinerCallback> mCallback;
 
     JoinerRouter mJoinerRouters[OPENTHREAD_CONFIG_JOINER_MAX_CANDIDATES];
     uint16_t     mJoinerRouterIndex;
 
     Coap::Message *mFinalizeMessage;
 
-    TimerMilli     mTimer;
-    Coap::Resource mJoinerEntrust;
+    JoinerTimer mTimer;
 };
 
+DeclareTmfHandler(Joiner, kUriJoinerEntrust);
+
 } // namespace MeshCoP
 
 DefineMapEnum(otJoinerState, MeshCoP::Joiner::State);
diff --git a/src/core/meshcop/joiner_router.cpp b/src/core/meshcop/joiner_router.cpp
index 679ae81..0961602 100644
--- a/src/core/meshcop/joiner_router.cpp
+++ b/src/core/meshcop/joiner_router.cpp
@@ -57,12 +57,10 @@
 JoinerRouter::JoinerRouter(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mSocket(aInstance)
-    , mRelayTransmit(UriPath::kRelayTx, &JoinerRouter::HandleRelayTransmit, this)
-    , mTimer(aInstance, JoinerRouter::HandleTimer)
+    , mTimer(aInstance)
     , mJoinerUdpPort(0)
     , mIsJoinerPortConfigured(false)
 {
-    Get<Tmf::Agent>().AddResource(mRelayTransmit);
 }
 
 void JoinerRouter::HandleNotifierEvents(Events aEvents)
@@ -132,17 +130,16 @@
 void JoinerRouter::HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error            error;
-    Coap::Message *  message = nullptr;
+    Coap::Message   *message = nullptr;
     Tmf::MessageInfo messageInfo(GetInstance());
     ExtendedTlv      tlv;
     uint16_t         borderAgentRloc;
-    uint16_t         offset;
 
     LogInfo("JoinerRouter::HandleUdpReceive");
 
     SuccessOrExit(error = GetBorderAgentRloc(Get<ThreadNetif>(), borderAgentRloc));
 
-    message = Get<Tmf::Agent>().NewPriorityNonConfirmablePostMessage(UriPath::kRelayRx);
+    message = Get<Tmf::Agent>().NewPriorityNonConfirmablePostMessage(kUriRelayRx);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = Tlv::Append<JoinerUdpPortTlv>(*message, aMessageInfo.GetPeerPort()));
@@ -152,26 +149,19 @@
     tlv.SetType(Tlv::kJoinerDtlsEncapsulation);
     tlv.SetLength(aMessage.GetLength() - aMessage.GetOffset());
     SuccessOrExit(error = message->Append(tlv));
-    offset = message->GetLength();
-    SuccessOrExit(error = message->SetLength(offset + tlv.GetLength()));
-    aMessage.CopyTo(aMessage.GetOffset(), offset, tlv.GetLength(), *message);
+    SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, aMessage.GetOffset(), tlv.GetLength()));
 
     messageInfo.SetSockAddrToRlocPeerAddrTo(borderAgentRloc);
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("Sent relay rx");
+    LogInfo("Sent %s", UriToString<kUriRelayRx>());
 
 exit:
     FreeMessageOnError(message, error);
 }
 
-void JoinerRouter::HandleRelayTransmit(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<JoinerRouter *>(aContext)->HandleRelayTransmit(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void JoinerRouter::HandleRelayTransmit(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void JoinerRouter::HandleTmf<kUriRelayTx>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     OT_UNUSED_VARIABLE(aMessageInfo);
 
@@ -181,13 +171,13 @@
     Kek                      kek;
     uint16_t                 offset;
     uint16_t                 length;
-    Message *                message = nullptr;
+    Message                 *message = nullptr;
     Message::Settings        settings(Message::kNoLinkSecurity, Message::kPriorityNet);
     Ip6::MessageInfo         messageInfo;
 
     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));
@@ -196,8 +186,7 @@
 
     VerifyOrExit((message = mSocket.NewMessage(0, settings)) != nullptr, error = kErrorNoBufs);
 
-    SuccessOrExit(error = message->SetLength(length));
-    aMessage.CopyTo(offset, 0, length, *message);
+    SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, offset, length));
 
     messageInfo.GetPeerAddr().SetToLinkLocalAddress(joinerIid);
     messageInfo.SetPeerPort(joinerPort);
@@ -218,7 +207,7 @@
 void JoinerRouter::DelaySendingJoinerEntrust(const Ip6::MessageInfo &aMessageInfo, const Kek &aKek)
 {
     Error                 error   = kErrorNone;
-    Message *             message = Get<MessagePool>().Allocate(Message::kTypeOther);
+    Message              *message = Get<MessagePool>().Allocate(Message::kTypeOther);
     JoinerEntrustMetadata metadata;
 
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
@@ -242,20 +231,12 @@
     LogError("schedule joiner entrust", error);
 }
 
-void JoinerRouter::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<JoinerRouter>().HandleTimer();
-}
-
-void JoinerRouter::HandleTimer(void)
-{
-    SendDelayedJoinerEntrust();
-}
+void JoinerRouter::HandleTimer(void) { SendDelayedJoinerEntrust(); }
 
 void JoinerRouter::SendDelayedJoinerEntrust(void)
 {
     JoinerEntrustMetadata metadata;
-    Message *             message = mDelayedJoinEnts.GetHead();
+    Message              *message = mDelayedJoinEnts.GetHead();
 
     VerifyOrExit(message != nullptr);
     VerifyOrExit(!mTimer.IsRunning());
@@ -292,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:
@@ -310,10 +290,10 @@
     Coap::Message *message = nullptr;
     Dataset        dataset;
     NetworkNameTlv networkName;
-    const Tlv *    tlv;
+    const Tlv     *tlv;
     NetworkKey     networkKey;
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kJoinerEntrust);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriJoinerEntrust);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     message->SetSubType(Message::kSubTypeJoinerEntrust);
@@ -380,8 +360,8 @@
     return message;
 }
 
-void JoinerRouter::HandleJoinerEntrustResponse(void *               aContext,
-                                               otMessage *          aMessage,
+void JoinerRouter::HandleJoinerEntrustResponse(void                *aContext,
+                                               otMessage           *aMessage,
                                                const otMessageInfo *aMessageInfo,
                                                Error                aResult)
 {
@@ -389,7 +369,7 @@
                                                                        AsCoreTypePtr(aMessageInfo), aResult);
 }
 
-void JoinerRouter::HandleJoinerEntrustResponse(Coap::Message *         aMessage,
+void JoinerRouter::HandleJoinerEntrustResponse(Coap::Message          *aMessage,
                                                const Ip6::MessageInfo *aMessageInfo,
                                                Error                   aResult)
 {
@@ -401,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/joiner_router.hpp b/src/core/meshcop/joiner_router.hpp
index e8c6116..4831c2f 100644
--- a/src/core/meshcop/joiner_router.hpp
+++ b/src/core/meshcop/joiner_router.hpp
@@ -38,7 +38,6 @@
 
 #if OPENTHREAD_FTD
 
-#include "coap/coap.hpp"
 #include "coap/coap_message.hpp"
 #include "common/locator.hpp"
 #include "common/message.hpp"
@@ -49,6 +48,7 @@
 #include "meshcop/meshcop_tlvs.hpp"
 #include "net/udp6.hpp"
 #include "thread/key_manager.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -57,6 +57,7 @@
 class JoinerRouter : public InstanceLocator, private NonCopyable
 {
     friend class ot::Notifier;
+    friend class Tmf::Agent;
 
 public:
     /**
@@ -101,17 +102,15 @@
     static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
     void        HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static void HandleRelayTransmit(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleRelayTransmit(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static void HandleJoinerEntrustResponse(void *               aContext,
-                                            otMessage *          aMessage,
+    static void HandleJoinerEntrustResponse(void                *aContext,
+                                            otMessage           *aMessage,
                                             const otMessageInfo *aMessageInfo,
                                             Error                aResult);
     void HandleJoinerEntrustResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult);
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTimer(void);
 
     void           Start(void);
     void           DelaySendingJoinerEntrust(const Ip6::MessageInfo &aMessageInfo, const Kek &aKek);
@@ -119,17 +118,20 @@
     Error          SendJoinerEntrust(const Ip6::MessageInfo &aMessageInfo);
     Coap::Message *PrepareJoinerEntrustMessage(void);
 
-    Ip6::Udp::Socket mSocket;
-    Coap::Resource   mRelayTransmit;
+    using JoinerRouterTimer = TimerMilliIn<JoinerRouter, &JoinerRouter::HandleTimer>;
 
-    TimerMilli   mTimer;
-    MessageQueue mDelayedJoinEnts;
+    Ip6::Udp::Socket mSocket;
+
+    JoinerRouterTimer mTimer;
+    MessageQueue      mDelayedJoinEnts;
 
     uint16_t mJoinerUdpPort;
 
     bool mIsJoinerPortConfigured : 1;
 };
 
+DeclareTmfHandler(JoinerRouter, kUriRelayTx);
+
 } // namespace MeshCoP
 } // namespace ot
 
diff --git a/src/core/meshcop/meshcop.cpp b/src/core/meshcop/meshcop.cpp
index a61b0ae..6fb17b0 100644
--- a/src/core/meshcop/meshcop.cpp
+++ b/src/core/meshcop/meshcop.cpp
@@ -37,7 +37,6 @@
 #include "common/debug.hpp"
 #include "common/locator_getters.hpp"
 #include "common/string.hpp"
-#include "crypto/pbkdf2_cmac.hpp"
 #include "crypto/sha256.hpp"
 #include "mac/mac_types.hpp"
 #include "thread/thread_netif.hpp"
@@ -174,11 +173,12 @@
     }
     else if (mLength <= sizeof(uint32_t) * CHAR_BIT)
     {
-        string.Append("0x%08x", static_cast<uint32_t>(mValue));
+        string.Append("0x%08lx", ToUlong(static_cast<uint32_t>(mValue)));
     }
     else
     {
-        string.Append("0x%x-%08x", static_cast<uint32_t>(mValue >> 32), static_cast<uint32_t>(mValue));
+        string.Append("0x%lx-%08lx", ToUlong(static_cast<uint32_t>(mValue >> 32)),
+                      ToUlong(static_cast<uint32_t>(mValue)));
     }
 
     string.Append("/len:%d", mLength);
@@ -316,14 +316,14 @@
 }
 
 #if OPENTHREAD_FTD
-Error GeneratePskc(const char *         aPassPhrase,
-                   const NetworkName &  aNetworkName,
+Error GeneratePskc(const char          *aPassPhrase,
+                   const NetworkName   &aNetworkName,
                    const ExtendedPanId &aExtPanId,
-                   Pskc &               aPskc)
+                   Pskc                &aPskc)
 {
     Error      error        = kErrorNone;
     const char saltPrefix[] = "Thread";
-    uint8_t    salt[Crypto::Pbkdf2::kMaxSaltLength];
+    uint8_t    salt[OT_CRYPTO_PBDKF2_MAX_SALT_SIZE];
     uint16_t   saltLen = 0;
     uint16_t   passphraseLen;
     uint8_t    networkNameLen;
@@ -348,8 +348,8 @@
     memcpy(salt + saltLen, aNetworkName.GetAsCString(), networkNameLen);
     saltLen += networkNameLen;
 
-    Crypto::Pbkdf2::GenerateKey(reinterpret_cast<const uint8_t *>(aPassPhrase), passphraseLen, salt, saltLen, 16384,
-                                OT_PSKC_MAX_SIZE, aPskc.m8);
+    otPlatCryptoPbkdf2GenerateKey(reinterpret_cast<const uint8_t *>(aPassPhrase), passphraseLen, salt, saltLen, 16384,
+                                  OT_PSKC_MAX_SIZE, aPskc.m8);
 
 exit:
     return error;
diff --git a/src/core/meshcop/meshcop.hpp b/src/core/meshcop/meshcop.hpp
index 0de1047..a1485db 100644
--- a/src/core/meshcop/meshcop.hpp
+++ b/src/core/meshcop/meshcop.hpp
@@ -420,10 +420,10 @@
  * @retval kErrorInvalidArgs   If the length of passphrase is out of range.
  *
  */
-Error GeneratePskc(const char *         aPassPhrase,
-                   const NetworkName &  aNetworkName,
+Error GeneratePskc(const char          *aPassPhrase,
+                   const NetworkName   &aNetworkName,
                    const ExtendedPanId &aExtPanId,
-                   Pskc &               aPskc);
+                   Pskc                &aPskc);
 
 /**
  * This function computes the Joiner ID from a factory-assigned IEEE EUI-64.
@@ -459,9 +459,7 @@
  */
 void LogError(const char *aActionText, Error aError);
 #else
-inline void LogError(const char *, Error)
-{
-}
+inline void LogError(const char *, Error) {}
 #endif
 
 } // namespace MeshCoP
diff --git a/src/core/meshcop/meshcop_leader.cpp b/src/core/meshcop/meshcop_leader.cpp
index a072026..5f97185 100644
--- a/src/core/meshcop/meshcop_leader.cpp
+++ b/src/core/meshcop/meshcop_leader.cpp
@@ -57,22 +57,13 @@
 
 Leader::Leader(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mPetition(UriPath::kLeaderPetition, Leader::HandlePetition, this)
-    , mKeepAlive(UriPath::kLeaderKeepAlive, Leader::HandleKeepAlive, this)
-    , mTimer(aInstance, HandleTimer)
+    , mTimer(aInstance)
     , mDelayTimerMinimal(DelayTimerTlv::kDelayTimerMinimal)
     , mSessionId(Random::NonCrypto::GetUint16())
 {
-    Get<Tmf::Agent>().AddResource(mPetition);
-    Get<Tmf::Agent>().AddResource(mKeepAlive);
 }
 
-void Leader::HandlePetition(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<Leader *>(aContext)->HandlePetition(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void Leader::HandlePetition(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void Leader::HandleTmf<kUriLeaderPetition>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     OT_UNUSED_VARIABLE(aMessageInfo);
 
@@ -80,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));
@@ -121,7 +112,7 @@
     SendPetitionResponse(aMessage, aMessageInfo, state);
 }
 
-void Leader::SendPetitionResponse(const Coap::Message &   aRequest,
+void Leader::SendPetitionResponse(const Coap::Message    &aRequest,
                                   const Ip6::MessageInfo &aMessageInfo,
                                   StateTlv::State         aState)
 {
@@ -145,26 +136,21 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, aMessageInfo));
 
-    LogInfo("sent petition response");
+    LogInfo("Sent %s response", UriToString<kUriLeaderPetition>());
 
 exit:
     FreeMessageOnError(message, error);
     LogError("send petition response", error);
 }
 
-void Leader::HandleKeepAlive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<Leader *>(aContext)->HandleKeepAlive(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void Leader::HandleKeepAlive(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void Leader::HandleTmf<kUriLeaderKeepAlive>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     uint8_t                state;
     uint16_t               sessionId;
     BorderAgentLocatorTlv *borderAgentLocator;
     StateTlv::State        responseState;
 
-    LogInfo("received keep alive");
+    LogInfo("Received %s", UriToString<kUriLeaderKeepAlive>());
 
     SuccessOrExit(Tlv::Find<StateTlv>(aMessage, state));
 
@@ -202,7 +188,7 @@
     return;
 }
 
-void Leader::SendKeepAliveResponse(const Coap::Message &   aRequest,
+void Leader::SendKeepAliveResponse(const Coap::Message    &aRequest,
                                    const Ip6::MessageInfo &aMessageInfo,
                                    StateTlv::State         aState)
 {
@@ -216,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);
@@ -227,15 +213,15 @@
 {
     Error            error = kErrorNone;
     Tmf::MessageInfo messageInfo(GetInstance());
-    Coap::Message *  message;
+    Coap::Message   *message;
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kDatasetChanged);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriDatasetChanged);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     messageInfo.SetSockAddrToRlocPeerAddrTo(aAddress);
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("sent dataset changed");
+    LogInfo("Sent %s", UriToString<kUriDatasetChanged>());
 
 exit:
     FreeMessageOnError(message, error);
@@ -253,15 +239,7 @@
     return error;
 }
 
-uint32_t Leader::GetDelayTimerMinimal(void) const
-{
-    return mDelayTimerMinimal;
-}
-
-void Leader::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Leader>().HandleTimer();
-}
+uint32_t Leader::GetDelayTimerMinimal(void) const { return mDelayTimerMinimal; }
 
 void Leader::HandleTimer(void)
 {
diff --git a/src/core/meshcop/meshcop_leader.hpp b/src/core/meshcop/meshcop_leader.hpp
index 75f0187..fa2a288 100644
--- a/src/core/meshcop/meshcop_leader.hpp
+++ b/src/core/meshcop/meshcop_leader.hpp
@@ -38,13 +38,13 @@
 
 #if OPENTHREAD_FTD
 
-#include "coap/coap.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "common/timer.hpp"
 #include "meshcop/meshcop_tlvs.hpp"
 #include "net/udp6.hpp"
 #include "thread/mle.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 namespace MeshCoP {
@@ -66,6 +66,8 @@
 
 class Leader : public InstanceLocator, private NonCopyable
 {
+    friend class Tmf::Agent;
+
 public:
     /**
      * This constructor initializes the Leader object.
@@ -111,28 +113,25 @@
 private:
     static constexpr uint32_t kTimeoutLeaderPetition = 50; // TIMEOUT_LEAD_PET (seconds)
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTimer(void);
 
-    static void HandlePetition(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandlePetition(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    void        SendPetitionResponse(const Coap::Message &   aRequest,
-                                     const Ip6::MessageInfo &aMessageInfo,
-                                     StateTlv::State         aState);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static void HandleKeepAlive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleKeepAlive(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    void        SendKeepAliveResponse(const Coap::Message &   aRequest,
-                                      const Ip6::MessageInfo &aMessageInfo,
-                                      StateTlv::State         aState);
+    void SendPetitionResponse(const Coap::Message    &aRequest,
+                              const Ip6::MessageInfo &aMessageInfo,
+                              StateTlv::State         aState);
+
+    void SendKeepAliveResponse(const Coap::Message    &aRequest,
+                               const Ip6::MessageInfo &aMessageInfo,
+                               StateTlv::State         aState);
 
     static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
 
     void ResignCommissioner(void);
 
-    Coap::Resource mPetition;
-    Coap::Resource mKeepAlive;
-    TimerMilli     mTimer;
+    using LeaderTimer = TimerMilliIn<Leader, &Leader::HandleTimer>;
+
+    LeaderTimer mTimer;
 
     uint32_t mDelayTimerMinimal;
 
@@ -140,6 +139,9 @@
     uint16_t          mSessionId;
 };
 
+DeclareTmfHandler(Leader, kUriLeaderPetition);
+DeclareTmfHandler(Leader, kUriLeaderKeepAlive);
+
 } // namespace MeshCoP
 } // namespace ot
 
diff --git a/src/core/meshcop/meshcop_tlvs.cpp b/src/core/meshcop/meshcop_tlvs.cpp
index c047ed9..8a0e3ee 100644
--- a/src/core/meshcop/meshcop_tlvs.cpp
+++ b/src/core/meshcop/meshcop_tlvs.cpp
@@ -35,6 +35,7 @@
 
 #include "common/const_cast.hpp"
 #include "common/debug.hpp"
+#include "common/num_utils.hpp"
 #include "common/string.hpp"
 #include "meshcop/meshcop.hpp"
 
@@ -135,10 +136,7 @@
     SetLength(len);
 }
 
-bool NetworkNameTlv::IsValid(void) const
-{
-    return GetLength() >= 1 && IsValidUtf8String(mNetworkName, GetLength());
-}
+bool NetworkNameTlv::IsValid(void) const { return IsValidUtf8String(mNetworkName, GetLength()); }
 
 void SteeringDataTlv::CopyTo(SteeringData &aSteeringData) const
 {
@@ -154,7 +152,7 @@
 SecurityPolicy SecurityPolicyTlv::GetSecurityPolicy(void) const
 {
     SecurityPolicy securityPolicy;
-    uint8_t        length = OT_MIN(static_cast<uint8_t>(sizeof(mFlags)), GetFlagsLength());
+    uint8_t        length = Min(static_cast<uint8_t>(sizeof(mFlags)), GetFlagsLength());
 
     securityPolicy.mRotationTime = GetRotationTime();
     securityPolicy.SetFlags(mFlags, length);
@@ -260,10 +258,7 @@
     return entry;
 }
 
-ChannelMaskEntryBase *ChannelMaskBaseTlv::GetFirstEntry(void)
-{
-    return AsNonConst(AsConst(this)->GetFirstEntry());
-}
+ChannelMaskEntryBase *ChannelMaskBaseTlv::GetFirstEntry(void) { return AsNonConst(AsConst(this)->GetFirstEntry()); }
 
 void ChannelMaskTlv::SetChannelMask(uint32_t aChannelMask)
 {
diff --git a/src/core/meshcop/meshcop_tlvs.hpp b/src/core/meshcop/meshcop_tlvs.hpp
index c21e799..1cd2755 100644
--- a/src/core/meshcop/meshcop_tlvs.hpp
+++ b/src/core/meshcop/meshcop_tlvs.hpp
@@ -44,6 +44,7 @@
 #include "common/const_cast.hpp"
 #include "common/encoding.hpp"
 #include "common/message.hpp"
+#include "common/num_utils.hpp"
 #include "common/string.hpp"
 #include "common/tlvs.hpp"
 #include "mac/mac_types.hpp"
@@ -120,6 +121,17 @@
     };
 
     /**
+     * Max length of Provisioning URL TLV.
+     *
+     */
+    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.
+    static constexpr uint8_t kMaxVendorSwVersionLength = 16; ///< Max length of Vendor SW Version TLV.
+    static constexpr uint8_t kMaxVendorDataLength      = 64; ///< Max length of Vendor Data TLV.
+
+    /**
      * This method returns the Type value.
      *
      * @returns The Type value.
@@ -1060,28 +1072,10 @@
  * This class implements State TLV generation and parsing.
  *
  */
-OT_TOOL_PACKED_BEGIN
-class StateTlv : public Tlv, public UintTlvInfo<Tlv::kState, uint8_t>
+class StateTlv : public UintTlvInfo<Tlv::kState, uint8_t>
 {
 public:
-    /**
-     * This method initializes the TLV.
-     *
-     */
-    void Init(void)
-    {
-        SetType(kState);
-        SetLength(sizeof(*this) - sizeof(Tlv));
-    }
-
-    /**
-     * This method indicates whether or not the TLV appears to be well-formed.
-     *
-     * @retval TRUE   If the TLV appears to be well-formed.
-     * @retval FALSE  If the TLV does not appear to be well-formed.
-     *
-     */
-    bool IsValid(void) const { return GetLength() >= sizeof(*this) - sizeof(Tlv); }
+    StateTlv(void) = delete;
 
     /**
      * State values.
@@ -1093,26 +1087,7 @@
         kPending = 0,    ///< Pending
         kAccept  = 1,    ///< Accept
     };
-
-    /**
-     * This method returns the State value.
-     *
-     * @returns The State value.
-     *
-     */
-    State GetState(void) const { return static_cast<State>(mState); }
-
-    /**
-     * This method sets the State value.
-     *
-     * @param[in]  aState  The State value.
-     *
-     */
-    void SetState(State aState) { mState = static_cast<uint8_t>(aState); }
-
-private:
-    uint8_t mState;
-} OT_TOOL_PACKED_END;
+};
 
 /**
  * This class implements Joiner UDP Port TLV generation and parsing.
@@ -1557,328 +1532,58 @@
      *
      */
     bool IsValid(void) const { return true; }
+
+    /**
+     * This method returns a pointer to the start of energy measurement list.
+     *
+     * @returns A pointer to the start start of energy energy measurement list.
+     *
+     */
+    const uint8_t *GetEnergyList(void) const { return mEnergyList; }
+
+    /**
+     * This method returns the length of energy measurement list.
+     *
+     * @returns The length of energy measurement list.
+     *
+     */
+    uint8_t GetEnergyListLength(void) const { return Min(kMaxListLength, GetLength()); }
+
+private:
+    static constexpr uint8_t kMaxListLength = OPENTHREAD_CONFIG_TMF_ENERGY_SCAN_MAX_RESULTS;
+
+    uint8_t mEnergyList[kMaxListLength];
 } OT_TOOL_PACKED_END;
 
 /**
- * This class implements Provisioning URL TLV generation and parsing.
+ * This class defines Provisioning TLV constants and types.
  *
  */
-OT_TOOL_PACKED_BEGIN
-class ProvisioningUrlTlv : public Tlv, public TlvInfo<Tlv::kProvisioningUrl>
-{
-public:
-    /**
-     * Maximum number of chars in the Provisioning URL string.
-     *
-     */
-    static constexpr uint16_t kMaxLength = OT_PROVISIONING_URL_MAX_SIZE;
-
-    /**
-     * This method initializes the TLV.
-     *
-     */
-    void Init(void)
-    {
-        SetType(kProvisioningUrl);
-        SetLength(0);
-    }
-
-    /*
-     * This method returns the Provisioning URL length.
-     *
-     * @returns The Provisioning URL length.
-     *
-     */
-    uint8_t GetProvisioningUrlLength(void) const
-    {
-        return GetLength() <= sizeof(mProvisioningUrl) ? GetLength() : sizeof(mProvisioningUrl);
-    }
-
-    /**
-     * This method indicates whether or not the TLV appears to be well-formed.
-     *
-     * @retval TRUE   If the TLV appears to be well-formed.
-     * @retval FALSE  If the TLV does not appear to be well-formed.
-     *
-     */
-    bool IsValid(void) const
-    {
-        return GetType() == kProvisioningUrl && mProvisioningUrl[GetProvisioningUrlLength()] == '\0';
-    }
-
-    /**
-     * This method returns the Provisioning URL value.
-     *
-     * @returns The Provisioning URL value.
-     *
-     */
-    const char *GetProvisioningUrl(void) const { return mProvisioningUrl; }
-
-    /**
-     * This method sets the Provisioning URL value.
-     *
-     * @param[in]  aProvisioningUrl  A pointer to the Provisioning URL value.
-     *
-     */
-    void SetProvisioningUrl(const char *aProvisioningUrl)
-    {
-        uint16_t len = aProvisioningUrl ? StringLength(aProvisioningUrl, kMaxLength) : 0;
-
-        SetLength(static_cast<uint8_t>(len));
-
-        if (len > 0)
-        {
-            memcpy(mProvisioningUrl, aProvisioningUrl, len);
-        }
-    }
-
-private:
-    char mProvisioningUrl[kMaxLength];
-} OT_TOOL_PACKED_END;
+typedef StringTlvInfo<Tlv::kProvisioningUrl, Tlv::kMaxProvisioningUrlLength> ProvisioningUrlTlv;
 
 /**
- * This class implements Vendor Name TLV generation and parsing.
+ * This class defines Vendor Name TLV constants and types.
  *
  */
-OT_TOOL_PACKED_BEGIN
-class VendorNameTlv : public Tlv, public TlvInfo<Tlv::kVendorName>
-{
-public:
-    /**
-     * This method initializes the TLV.
-     *
-     */
-    void Init(void)
-    {
-        SetType(kVendorName);
-        SetLength(0);
-    }
-
-    /**
-     * This method returns the Vendor Name length.
-     *
-     * @returns The Vendor Name length.
-     *
-     */
-    uint8_t GetVendorNameLength(void) const
-    {
-        return GetLength() <= sizeof(mVendorName) ? GetLength() : sizeof(mVendorName);
-    }
-
-    /**
-     * This method returns the Vendor Name value.
-     *
-     * @returns The Vendor Name value.
-     *
-     */
-    const char *GetVendorName(void) const { return mVendorName; }
-
-    /**
-     * This method sets the Vendor Name value.
-     *
-     * @param[in]  aVendorName  A pointer to the Vendor Name value.
-     *
-     */
-    void SetVendorName(const char *aVendorName)
-    {
-        uint16_t len = (aVendorName == nullptr) ? 0 : StringLength(aVendorName, sizeof(mVendorName));
-
-        SetLength(static_cast<uint8_t>(len));
-
-        if (len > 0)
-        {
-            memcpy(mVendorName, aVendorName, len);
-        }
-    }
-
-private:
-    static constexpr uint8_t kMaxLength = 32;
-
-    char mVendorName[kMaxLength];
-} OT_TOOL_PACKED_END;
+typedef StringTlvInfo<Tlv::kVendorName, Tlv::kMaxVendorNameLength> VendorNameTlv;
 
 /**
- * This class implements Vendor Model TLV generation and parsing.
+ * This class defines Vendor Model TLV constants and types.
  *
  */
-OT_TOOL_PACKED_BEGIN
-class VendorModelTlv : public Tlv, public TlvInfo<Tlv::kVendorModel>
-{
-public:
-    /**
-     * This method initializes the TLV.
-     *
-     */
-    void Init(void)
-    {
-        SetType(kVendorModel);
-        SetLength(0);
-    }
-
-    /**
-     * This method returns the Vendor Model length.
-     *
-     * @returns The Vendor Model length.
-     *
-     */
-    uint8_t GetVendorModelLength(void) const
-    {
-        return GetLength() <= sizeof(mVendorModel) ? GetLength() : sizeof(mVendorModel);
-    }
-
-    /**
-     * This method returns the Vendor Model value.
-     *
-     * @returns The Vendor Model value.
-     *
-     */
-    const char *GetVendorModel(void) const { return mVendorModel; }
-
-    /**
-     * This method sets the Vendor Model value.
-     *
-     * @param[in]  aVendorModel  A pointer to the Vendor Model value.
-     *
-     */
-    void SetVendorModel(const char *aVendorModel)
-    {
-        uint16_t len = (aVendorModel == nullptr) ? 0 : StringLength(aVendorModel, sizeof(mVendorModel));
-
-        SetLength(static_cast<uint8_t>(len));
-
-        if (len > 0)
-        {
-            memcpy(mVendorModel, aVendorModel, len);
-        }
-    }
-
-private:
-    static constexpr uint8_t kMaxLength = 32;
-
-    char mVendorModel[kMaxLength];
-} OT_TOOL_PACKED_END;
+typedef StringTlvInfo<Tlv::kVendorModel, Tlv::kMaxVendorModelLength> VendorModelTlv;
 
 /**
- * This class implements Vendor SW Version TLV generation and parsing.
+ * This class defines Vendor SW Version TLV constants and types.
  *
  */
-OT_TOOL_PACKED_BEGIN
-class VendorSwVersionTlv : public Tlv, public TlvInfo<Tlv::kVendorSwVersion>
-{
-public:
-    /**
-     * This method initializes the TLV.
-     *
-     */
-    void Init(void)
-    {
-        SetType(kVendorSwVersion);
-        SetLength(0);
-    }
-
-    /**
-     * This method returns the Vendor SW Version length.
-     *
-     * @returns The Vendor SW Version length.
-     *
-     */
-    uint8_t GetVendorSwVersionLength(void) const
-    {
-        return GetLength() <= sizeof(mVendorSwVersion) ? GetLength() : sizeof(mVendorSwVersion);
-    }
-
-    /**
-     * This method returns the Vendor SW Version value.
-     *
-     * @returns The Vendor SW Version value.
-     *
-     */
-    const char *GetVendorSwVersion(void) const { return mVendorSwVersion; }
-
-    /**
-     * This method sets the Vendor SW Version value.
-     *
-     * @param[in]  aVendorSwVersion  A pointer to the Vendor SW Version value.
-     *
-     */
-    void SetVendorSwVersion(const char *aVendorSwVersion)
-    {
-        uint16_t len = (aVendorSwVersion == nullptr) ? 0 : StringLength(aVendorSwVersion, sizeof(mVendorSwVersion));
-
-        SetLength(static_cast<uint8_t>(len));
-
-        if (len > 0)
-        {
-            memcpy(mVendorSwVersion, aVendorSwVersion, len);
-        }
-    }
-
-private:
-    static constexpr uint8_t kMaxLength = 16;
-
-    char mVendorSwVersion[kMaxLength];
-} OT_TOOL_PACKED_END;
+typedef StringTlvInfo<Tlv::kVendorSwVersion, Tlv::kMaxVendorSwVersionLength> VendorSwVersionTlv;
 
 /**
- * This class implements Vendor Data TLV generation and parsing.
+ * This class defines Vendor Data TLV constants and types.
  *
  */
-OT_TOOL_PACKED_BEGIN
-class VendorDataTlv : public Tlv, public TlvInfo<Tlv::kVendorData>
-{
-public:
-    /**
-     * This method initializes the TLV.
-     *
-     */
-    void Init(void)
-    {
-        SetType(kVendorData);
-        SetLength(0);
-    }
-
-    /**
-     * This method returns the Vendor Data length.
-     *
-     * @returns The Vendor Data length.
-     *
-     */
-    uint8_t GetVendorDataLength(void) const
-    {
-        return GetLength() <= sizeof(mVendorData) ? GetLength() : sizeof(mVendorData);
-    }
-
-    /**
-     * This method returns the Vendor Data value.
-     *
-     * @returns The Vendor Data value.
-     *
-     */
-    const char *GetVendorData(void) const { return mVendorData; }
-
-    /**
-     * This method sets the Vendor Data value.
-     *
-     * @param[in]  aVendorData  A pointer to the Vendor Data value.
-     *
-     */
-    void SetVendorData(const char *aVendorData)
-    {
-        uint16_t len = (aVendorData == nullptr) ? 0 : StringLength(aVendorData, sizeof(mVendorData));
-
-        SetLength(static_cast<uint8_t>(len));
-
-        if (len > 0)
-        {
-            memcpy(mVendorData, aVendorData, len);
-        }
-    }
-
-private:
-    static constexpr uint8_t kMaxLength = 64;
-
-    char mVendorData[kMaxLength];
-} OT_TOOL_PACKED_END;
+typedef StringTlvInfo<Tlv::kVendorData, Tlv::kMaxVendorDataLength> VendorDataTlv;
 
 /**
  * This class implements Vendor Stack Version TLV generation and parsing.
@@ -2029,33 +1734,20 @@
 } OT_TOOL_PACKED_END;
 
 /**
- * This class implements UDP Encapsulation TLV generation and parsing.
+ * This class defines UDP Encapsulation TLV types and constants.
+ *
+ */
+typedef TlvInfo<MeshCoP::Tlv::kUdpEncapsulation> UdpEncapsulationTlv;
+
+/**
+ * This class represents UDP Encapsulation TLV value header (source and destination ports).
  *
  */
 OT_TOOL_PACKED_BEGIN
-class UdpEncapsulationTlv : public ExtendedTlv, public TlvInfo<MeshCoP::Tlv::kUdpEncapsulation>
+class UdpEncapsulationTlvHeader
 {
 public:
     /**
-     * This method initializes the TLV.
-     *
-     */
-    void Init(void)
-    {
-        SetType(MeshCoP::Tlv::kUdpEncapsulation);
-        SetLength(sizeof(*this) - sizeof(ExtendedTlv));
-    }
-
-    /**
-     * This method indicates whether or not the TLV appears to be well-formed.
-     *
-     * @retval TRUE   If the TLV appears to be well-formed.
-     * @retval FALSE  If the TLV does not appear to be well-formed.
-     *
-     */
-    bool IsValid(void) const { return GetLength() >= sizeof(*this) - sizeof(ExtendedTlv); }
-
-    /**
      * This method returns the source port.
      *
      * @returns The source port.
@@ -2087,25 +1779,10 @@
      */
     void SetDestinationPort(uint16_t aDestinationPort) { mDestinationPort = HostSwap16(aDestinationPort); }
 
-    /**
-     * This method returns the calculated UDP length.
-     *
-     * @returns The calculated UDP length.
-     *
-     */
-    uint16_t GetUdpLength(void) const { return GetLength() - sizeof(mSourcePort) - sizeof(mDestinationPort); }
-
-    /**
-     * This method updates the UDP length.
-     *
-     * @param[in]   aLength     The length of UDP payload in bytes.
-     *
-     */
-    void SetUdpLength(uint16_t aLength) { SetLength(sizeof(mSourcePort) + sizeof(mDestinationPort) + aLength); }
-
 private:
     uint16_t mSourcePort;
     uint16_t mDestinationPort;
+    // Followed by the UDP Payload.
 } OT_TOOL_PACKED_END;
 
 /**
diff --git a/src/core/meshcop/network_name.cpp b/src/core/meshcop/network_name.cpp
index e4ae562..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;
@@ -71,6 +65,9 @@
     // chars. The `+ 1` ensures that a `aNameString` with length
     // longer than `kMaxSize` is correctly rejected (returning error
     // `kErrorInvalidArgs`).
+    // Additionally, no minimum length is verified in order to ensure
+    // backwards compatibility with previous versions that allowed
+    // a zero-length name.
 
     Error    error;
     NameData data(aNameString, kMaxSize + 1);
@@ -89,7 +86,7 @@
     NameData data   = aNameData;
     uint8_t  newLen = static_cast<uint8_t>(StringLength(data.GetBuffer(), data.GetLength()));
 
-    VerifyOrExit((0 < newLen) && (newLen <= kMaxSize), error = kErrorInvalidArgs);
+    VerifyOrExit(newLen <= kMaxSize, error = kErrorInvalidArgs);
 
     data.SetLength(newLen);
 
@@ -106,18 +103,15 @@
     return error;
 }
 
-bool NetworkName::operator==(const NetworkName &aOther) const
-{
-    return GetAsData() == aOther.GetAsData();
-}
+bool NetworkName::operator==(const NetworkName &aOther) const { return GetAsData() == aOther.GetAsData(); }
 
 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 7bacc80..8c6a004 100644
--- a/src/core/meshcop/panid_query_client.cpp
+++ b/src/core/meshcop/panid_query_client.cpp
@@ -53,28 +53,24 @@
 
 PanIdQueryClient::PanIdQueryClient(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mCallback(nullptr)
-    , mContext(nullptr)
-    , mPanIdQuery(UriPath::kPanIdConflict, &PanIdQueryClient::HandleConflict, this)
 {
-    Get<Tmf::Agent>().AddResource(mPanIdQuery);
 }
 
 Error PanIdQueryClient::SendQuery(uint16_t                            aPanId,
                                   uint32_t                            aChannelMask,
-                                  const Ip6::Address &                aAddress,
+                                  const Ip6::Address                 &aAddress,
                                   otCommissionerPanIdConflictCallback aCallback,
-                                  void *                              aContext)
+                                  void                               *aContext)
 {
     Error                   error = kErrorNone;
     MeshCoP::ChannelMaskTlv channelMask;
     Tmf::MessageInfo        messageInfo(GetInstance());
-    Coap::Message *         message = nullptr;
+    Coap::Message          *message = nullptr;
 
     VerifyOrExit(Get<MeshCoP::Commissioner>().IsActive(), error = kErrorInvalidState);
     VerifyOrExit((message = Get<Tmf::Agent>().NewPriorityMessage()) != nullptr, error = kErrorNoBufs);
 
-    SuccessOrExit(error = message->InitAsPost(aAddress, UriPath::kPanIdQuery));
+    SuccessOrExit(error = message->InitAsPost(aAddress, kUriPanIdQuery));
     SuccessOrExit(error = message->SetPayloadMarker());
 
     SuccessOrExit(
@@ -89,43 +85,34 @@
     messageInfo.SetSockAddrToRlocPeerAddrTo(aAddress);
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("sent panid query");
+    LogInfo("Sent %s", UriToString<kUriPanIdQuery>());
 
-    mCallback = aCallback;
-    mContext  = aContext;
+    mCallback.Set(aCallback, aContext);
 
 exit:
     FreeMessageOnError(message, error);
     return error;
 }
 
-void PanIdQueryClient::HandleConflict(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
+template <>
+void PanIdQueryClient::HandleTmf<kUriPanIdConflict>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    static_cast<PanIdQueryClient *>(aContext)->HandleConflict(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void PanIdQueryClient::HandleConflict(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));
 
     VerifyOrExit((mask = MeshCoP::ChannelMaskTlv::GetChannelMask(aMessage)) != 0);
 
-    if (mCallback != nullptr)
-    {
-        mCallback(panId, mask, mContext);
-    }
+    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/meshcop/panid_query_client.hpp b/src/core/meshcop/panid_query_client.hpp
index 3507150..46b1ff7 100644
--- a/src/core/meshcop/panid_query_client.hpp
+++ b/src/core/meshcop/panid_query_client.hpp
@@ -40,10 +40,11 @@
 
 #include <openthread/commissioner.h>
 
-#include "coap/coap.hpp"
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "net/ip6_address.hpp"
 #include "net/udp6.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -53,6 +54,8 @@
  */
 class PanIdQueryClient : public InstanceLocator
 {
+    friend class Tmf::Agent;
+
 public:
     /**
      * This constructor initializes the object.
@@ -75,20 +78,18 @@
      */
     Error SendQuery(uint16_t                            aPanId,
                     uint32_t                            aChannelMask,
-                    const Ip6::Address &                aAddress,
+                    const Ip6::Address                 &aAddress,
                     otCommissionerPanIdConflictCallback aCallback,
-                    void *                              aContext);
+                    void                               *aContext);
 
 private:
-    static void HandleConflict(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleConflict(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    otCommissionerPanIdConflictCallback mCallback;
-    void *                              mContext;
-
-    Coap::Resource mPanIdQuery;
+    Callback<otCommissionerPanIdConflictCallback> mCallback;
 };
 
+DeclareTmfHandler(PanIdQueryClient, kUriPanIdConflict);
+
 /**
  * @}
  */
diff --git a/src/core/meshcop/timestamp.cpp b/src/core/meshcop/timestamp.cpp
index 08ed36b..c0f356e 100644
--- a/src/core/meshcop/timestamp.cpp
+++ b/src/core/meshcop/timestamp.cpp
@@ -34,6 +34,7 @@
 #include "timestamp.hpp"
 
 #include "common/code_utils.hpp"
+#include "common/num_utils.hpp"
 
 namespace ot {
 namespace MeshCoP {
@@ -80,39 +81,15 @@
 
 int Timestamp::Compare(const Timestamp &aFirst, const Timestamp &aSecond)
 {
-    int      rval;
-    uint64_t firstSeconds;
-    uint64_t secondSeconds;
-    uint16_t firstTicks;
-    uint16_t secondTicks;
-    bool     firstAuthoritative;
-    bool     secondAuthoritative;
+    int rval;
 
-    firstSeconds  = aFirst.GetSeconds();
-    secondSeconds = aSecond.GetSeconds();
+    rval = ThreeWayCompare(aFirst.GetSeconds(), aSecond.GetSeconds());
+    VerifyOrExit(rval == 0);
 
-    if (firstSeconds != secondSeconds)
-    {
-        ExitNow(rval = (firstSeconds > secondSeconds) ? 1 : -1);
-    }
+    rval = ThreeWayCompare(aFirst.GetTicks(), aSecond.GetTicks());
+    VerifyOrExit(rval == 0);
 
-    firstTicks  = aFirst.GetTicks();
-    secondTicks = aSecond.GetTicks();
-
-    if (firstTicks != secondTicks)
-    {
-        ExitNow(rval = (firstTicks > secondTicks) ? 1 : -1);
-    }
-
-    firstAuthoritative  = aFirst.GetAuthoritative();
-    secondAuthoritative = aSecond.GetAuthoritative();
-
-    if (firstAuthoritative != secondAuthoritative)
-    {
-        ExitNow(rval = firstAuthoritative ? 1 : -1);
-    }
-
-    rval = 0;
+    rval = ThreeWayCompare(aFirst.GetAuthoritative(), aSecond.GetAuthoritative());
 
 exit:
     return rval;
diff --git a/src/core/mtd.cmake b/src/core/mtd.cmake
index 7d6f0ef..e9b831c 100644
--- a/src/core/mtd.cmake
+++ b/src/core/mtd.cmake
@@ -43,9 +43,8 @@
 target_link_libraries(openthread-mtd
     PRIVATE
         ${OT_MBEDTLS}
+        ot-config-mtd
         ot-config
 )
 
-if(NOT OT_EXCLUDE_TCPLP_LIB)
-    target_link_libraries(openthread-mtd PRIVATE tcplp)
-endif()
+target_link_libraries(openthread-mtd PRIVATE tcplp-mtd)
diff --git a/src/core/net/checksum.cpp b/src/core/net/checksum.cpp
index 16860cb..df761d4 100644
--- a/src/core/net/checksum.cpp
+++ b/src/core/net/checksum.cpp
@@ -93,7 +93,7 @@
 void Checksum::Calculate(const Ip6::Address &aSource,
                          const Ip6::Address &aDestination,
                          uint8_t             aIpProto,
-                         const Message &     aMessage)
+                         const Message      &aMessage)
 {
     Message::Chunk chunk;
     uint16_t       length = aMessage.GetLength() - aMessage.GetOffset();
@@ -119,13 +119,13 @@
 void Checksum::Calculate(const Ip4::Address &aSource,
                          const Ip4::Address &aDestination,
                          uint8_t             aIpProto,
-                         const Message &     aMessage)
+                         const Message      &aMessage)
 {
     Message::Chunk chunk;
     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));
@@ -154,7 +154,7 @@
     return (checksum.GetValue() == kValidRxChecksum) ? kErrorNone : kErrorDrop;
 }
 
-void Checksum::UpdateMessageChecksum(Message &           aMessage,
+void Checksum::UpdateMessageChecksum(Message            &aMessage,
                                      const Ip6::Address &aSource,
                                      const Ip6::Address &aDestination,
                                      uint8_t             aIpProto)
@@ -189,7 +189,7 @@
     return;
 }
 
-void Checksum::UpdateMessageChecksum(Message &           aMessage,
+void Checksum::UpdateMessageChecksum(Message            &aMessage,
                                      const Ip4::Address &aSource,
                                      const Ip4::Address &aDestination,
                                      uint8_t             aIpProto)
diff --git a/src/core/net/checksum.hpp b/src/core/net/checksum.hpp
index f335785..06d6e69 100644
--- a/src/core/net/checksum.hpp
+++ b/src/core/net/checksum.hpp
@@ -80,7 +80,7 @@
      * @param[in] aIpProto      The Internet Protocol value.
      *
      */
-    static void UpdateMessageChecksum(Message &           aMessage,
+    static void UpdateMessageChecksum(Message            &aMessage,
                                       const Ip6::Address &aSource,
                                       const Ip6::Address &aDestination,
                                       uint8_t             aIpProto);
@@ -96,7 +96,7 @@
      * @param[in] aIpProto      The Internet Protocol value.
      *
      */
-    static void UpdateMessageChecksum(Message &           aMessage,
+    static void UpdateMessageChecksum(Message            &aMessage,
                                       const Ip4::Address &aSource,
                                       const Ip4::Address &aDestination,
                                       uint8_t             aIpProto);
@@ -124,11 +124,11 @@
     void     Calculate(const Ip6::Address &aSource,
                        const Ip6::Address &aDestination,
                        uint8_t             aIpProto,
-                       const Message &     aMessage);
+                       const Message      &aMessage);
     void     Calculate(const Ip4::Address &aSource,
                        const Ip4::Address &aDestination,
                        uint8_t             aIpProto,
-                       const Message &     aMessage);
+                       const Message      &aMessage);
 
     static constexpr uint16_t kValidRxChecksum = 0xffff;
 
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 9c3e772..db67135 100644
--- a/src/core/net/dhcp6_client.cpp
+++ b/src/core/net/dhcp6_client.cpp
@@ -218,10 +218,7 @@
     return rval;
 }
 
-void Client::HandleTrickleTimer(TrickleTimer &aTrickleTimer)
-{
-    aTrickleTimer.Get<Client>().HandleTrickleTimer();
-}
+void Client::HandleTrickleTimer(TrickleTimer &aTrickleTimer) { aTrickleTimer.Get<Client>().HandleTrickleTimer(); }
 
 void Client::HandleTrickleTimer(void)
 {
@@ -263,10 +260,10 @@
 void Client::Solicit(uint16_t aRloc16)
 {
     Error            error = kErrorNone;
-    Message *        message;
+    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_client.hpp b/src/core/net/dhcp6_client.hpp
index 7004cef..533ded7 100644
--- a/src/core/net/dhcp6_client.hpp
+++ b/src/core/net/dhcp6_client.hpp
@@ -110,7 +110,7 @@
     void Stop(void);
 
     static bool MatchNetifAddressWithPrefix(const Ip6::Netif::UnicastAddress &aNetifAddress,
-                                            const Ip6::Prefix &               aIp6Prefix);
+                                            const Ip6::Prefix                &aIp6Prefix);
 
     void Solicit(uint16_t aRloc16);
 
diff --git a/src/core/net/dhcp6_server.cpp b/src/core/net/dhcp6_server.cpp
index eaab1b4..826d5fe 100644
--- a/src/core/net/dhcp6_server.cpp
+++ b/src/core/net/dhcp6_server.cpp
@@ -145,10 +145,7 @@
     return;
 }
 
-void Server::Stop(void)
-{
-    IgnoreError(mSocket.Close());
-}
+void Server::Stop(void) { IgnoreError(mSocket.Close()); }
 
 void Server::AddPrefixAgent(const Ip6::Prefix &aIp6Prefix, const Lowpan::Context &aContext)
 {
@@ -333,16 +330,16 @@
     return error;
 }
 
-Error Server::SendReply(const Ip6::Address & aDst,
+Error Server::SendReply(const Ip6::Address  &aDst,
                         const TransactionId &aTransactionId,
-                        ClientIdentifier &   aClientId,
-                        IaNa &               aIaNa)
+                        ClientIdentifier    &aClientId,
+                        IaNa                &aIaNa)
 {
     Error            error = kErrorNone;
     Ip6::MessageInfo messageInfo;
-    Message *        message;
+    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));
@@ -473,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/dhcp6_server.hpp b/src/core/net/dhcp6_server.hpp
index 8fa7db3..fa14e8b 100644
--- a/src/core/net/dhcp6_server.hpp
+++ b/src/core/net/dhcp6_server.hpp
@@ -209,10 +209,10 @@
     Error    ProcessIaAddress(Message &aMessage, uint16_t aOffset);
     Error    ProcessElapsedTime(Message &aMessage, uint16_t aOffset);
 
-    Error SendReply(const Ip6::Address & aDst,
+    Error SendReply(const Ip6::Address  &aDst,
                     const TransactionId &aTransactionId,
-                    ClientIdentifier &   aClientId,
-                    IaNa &               aIaNa);
+                    ClientIdentifier    &aClientId,
+                    IaNa                &aIaNa);
 
     Ip6::Udp::Socket mSocket;
 
diff --git a/src/core/net/dns_client.cpp b/src/core/net/dns_client.cpp
index bb33d70..da16725 100644
--- a/src/core/net/dns_client.cpp
+++ b/src/core/net/dns_client.cpp
@@ -49,6 +49,11 @@
 namespace ot {
 namespace Dns {
 
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+using ot::Encoding::BigEndian::ReadUint16;
+using ot::Encoding::BigEndian::WriteUint16;
+#endif
+
 RegisterLogModule("DnsClient");
 
 //---------------------------------------------------------------------------------------------------------------------
@@ -65,18 +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())
     {
@@ -109,6 +123,19 @@
         SetNat64Mode(aDefaultConfig.GetNat64Mode());
     }
 #endif
+
+    if (GetServiceMode() == kServiceModeUnspecified)
+    {
+        SetServiceMode(aDefaultConfig.GetServiceMode());
+    }
+
+    if (GetTransportProto() == kDnsTransportUnspecified)
+    {
+        SetTransportProto(aDefaultConfig.GetTransportProto());
+    }
+
+exit:
+    return;
 }
 
 //---------------------------------------------------------------------------------------------------------------------
@@ -176,10 +203,10 @@
 }
 
 Error Client::Response::FindHostAddress(Section       aSection,
-                                        const Name &  aHostName,
+                                        const Name   &aHostName,
                                         uint16_t      aIndex,
                                         Ip6::Address &aAddress,
-                                        uint32_t &    aTtl) const
+                                        uint32_t     &aTtl) const
 {
     Error      error;
     uint16_t   offset;
@@ -220,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);
 
@@ -267,46 +314,97 @@
 
     if (error == kErrorNotFound)
     {
-        AsCoreType(&aServiceInfo.mHostAddress).Clear();
-        aServiceInfo.mHostAddressTtl = 0;
-    }
-    else
-    {
-        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);
-    error = ResourceRecord::FindRecord(*mMessage, offset, numRecords, /* aIndex */ 0, aName, txtRecord);
-
-    switch (error)
-    {
-    case kErrorNone:
-        SuccessOrExit(error =
-                          txtRecord.ReadTxtData(*mMessage, offset, aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize));
-        aServiceInfo.mTxtDataTtl = txtRecord.GetTtl();
-        break;
-
-    case kErrorNotFound:
-        aServiceInfo.mTxtDataSize = 0;
-        aServiceInfo.mTxtDataTtl  = 0;
-        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
 
@@ -380,21 +478,29 @@
     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;
 }
 
-Error Client::BrowseResponse::GetHostAddress(const char *  aHostName,
+Error Client::BrowseResponse::GetHostAddress(const char   *aHostName,
                                              uint16_t      aIndex,
                                              Ip6::Address &aAddress,
-                                             uint32_t &    aTtl) const
+                                             uint32_t     &aTtl) const
 {
     return FindHostAddress(kAdditionalDataSection, Name(aHostName), aIndex, aAddress, aTtl);
 }
@@ -458,9 +564,9 @@
 //---------------------------------------------------------------------------------------------------------------------
 // Client::ServiceResponse
 
-Error Client::ServiceResponse::GetServiceName(char *   aLabelBuffer,
+Error Client::ServiceResponse::GetServiceName(char    *aLabelBuffer,
                                               uint8_t  aLabelBufferSize,
-                                              char *   aNameBuffer,
+                                              char    *aNameBuffer,
                                               uint16_t aNameBufferSize) const
 {
     Error    error;
@@ -477,18 +583,71 @@
 
 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);
+
+        // 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,
+Error Client::ServiceResponse::GetHostAddress(const char   *aHostName,
                                               uint16_t      aIndex,
                                               Ip6::Address &aAddress,
-                                              uint32_t &    aTtl) const
+                                              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)
+    {
+        error = response->FindHostAddress(kAdditionalDataSection, Name(aHostName), aIndex, aAddress, aTtl);
+
+        if (error == kErrorNone)
+        {
+            break;
+        }
+    }
+
+    return error;
 }
 
 #endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
@@ -506,31 +665,39 @@
 #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
 };
 
 Client::Client(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mSocket(aInstance)
-    , mTimer(aInstance, Client::HandleTimer)
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+    , mTcpState(kTcpUninitialized)
+#endif
+    , mTimer(aInstance)
     , mDefaultConfig(QueryConfig::kInitFromDefaults)
 #if OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE
     , mUserDidSetDefaultAddress(false)
@@ -541,11 +708,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
 }
 
@@ -554,7 +725,7 @@
     Error error;
 
     SuccessOrExit(error = mSocket.Open(&Client::HandleUdpReceive, this));
-    SuccessOrExit(error = mSocket.Bind(0, OT_NETIF_UNSPECIFIED));
+    SuccessOrExit(error = mSocket.Bind(0, Ip6::kNetifUnspecified));
 
 exit:
     return error;
@@ -564,19 +735,50 @@
 {
     Query *query;
 
-    while ((query = mQueries.GetHead()) != nullptr)
+    while ((query = mMainQueries.GetHead()) != nullptr)
     {
         FinalizeQuery(*query, kErrorAbort);
     }
 
     IgnoreError(mSocket.Close());
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+    if (mTcpState != kTcpUninitialized)
+    {
+        IgnoreError(mEndpoint.Deinitialize());
+    }
+#endif
 }
 
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+Error Client::InitTcpSocket(void)
+{
+    Error                       error;
+    otTcpEndpointInitializeArgs endpointArgs;
+
+    memset(&endpointArgs, 0x00, sizeof(endpointArgs));
+    endpointArgs.mSendDoneCallback         = HandleTcpSendDoneCallback;
+    endpointArgs.mEstablishedCallback      = HandleTcpEstablishedCallback;
+    endpointArgs.mReceiveAvailableCallback = HandleTcpReceiveAvailableCallback;
+    endpointArgs.mDisconnectedCallback     = HandleTcpDisconnectedCallback;
+    endpointArgs.mContext                  = this;
+    endpointArgs.mReceiveBuffer            = mReceiveBufferBytes;
+    endpointArgs.mReceiveBufferSize        = sizeof(mReceiveBufferBytes);
+
+    mSendLink.mNext   = nullptr;
+    mSendLink.mData   = mSendBufferBytes;
+    mSendLink.mLength = 0;
+
+    SuccessOrExit(error = mEndpoint.Initialize(Get<Instance>(), endpointArgs));
+exit:
+    return error;
+}
+#endif
+
 void Client::SetDefaultConfig(const QueryConfig &aQueryConfig)
 {
     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();
@@ -607,33 +809,37 @@
 }
 #endif
 
-Error Client::ResolveAddress(const char *       aHostName,
+Error Client::ResolveAddress(const char        *aHostName,
                              AddressCallback    aCallback,
-                             void *             aContext,
+                             void              *aContext,
                              const QueryConfig *aConfig)
 {
     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
-Error Client::ResolveIp4Address(const char *       aHostName,
+Error Client::ResolveIp4Address(const char        *aHostName,
                                 AddressCallback    aCallback,
-                                void *             aContext,
+                                void              *aContext,
                                 const QueryConfig *aConfig)
 {
     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
 
@@ -644,28 +850,56 @@
     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,
-                             const char *       aServiceName,
+Error Client::ResolveService(const char        *aInstanceLabel,
+                             const char        *aServiceName,
                              ServiceCallback    aCallback,
-                             void *             aContext,
+                             void              *aContext,
                              const QueryConfig *aConfig)
 {
     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);
+
+    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;
+        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;
@@ -673,37 +907,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)
     {
@@ -716,9 +930,34 @@
 #endif
 
     SuccessOrExit(error = AllocateQuery(aInfo, aLabel, aName, query));
-    mQueries.Enqueue(*query);
 
-    SendQuery(*query, aInfo, /* aUpdateTimer */ true);
+    mMainQueries.Enqueue(*query);
+
+    error = SendQuery(*query, aInfo, /* aUpdateTimer */ true);
+    VerifyOrExit(error == kErrorNone, FreeQuery(*query));
+
+    if (aSecondType != kNoQuery)
+    {
+        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:
     return error;
@@ -728,6 +967,10 @@
 {
     Error error = kErrorNone;
 
+    aQuery = nullptr;
+
+    VerifyOrExit(aInfo.mConfig.GetResponseTimeout() <= TimerMilli::kMaxDelay, error = kErrorInvalidArgs);
+
     aQuery = Get<MessagePool>().Allocate(Message::kTypeOther);
     VerifyOrExit(aQuery != nullptr, error = kErrorNoBufs);
 
@@ -745,12 +988,31 @@
     return error;
 }
 
-void Client::FreeQuery(Query &aQuery)
+Client::Query &Client::FindMainQuery(Query &aQuery)
 {
-    mQueries.DequeueAndFree(aQuery);
+    QueryInfo info;
+
+    info.ReadFrom(aQuery);
+
+    return (info.mMainQuery == nullptr) ? aQuery : *info.mMainQuery;
 }
 
-void Client::SendQuery(Query &aQuery, QueryInfo &aInfo, bool aUpdateTimer)
+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)
 {
     // This method prepares and sends a query message represented by
     // `aQuery` and `aInfo`. This method updates `aInfo` (e.g., sets
@@ -760,9 +1022,10 @@
     // is handled by caller).
 
     Error            error   = kErrorNone;
-    Message *        message = nullptr;
+    Message         *message = nullptr;
     Header           header;
     Ip6::MessageInfo messageInfo;
+    uint16_t         length = 0;
 
     aInfo.mTransmissionCount++;
     aInfo.mRetransmissionTime = TimerMilli::GetNow() + aInfo.mConfig.GetResponseTimeout();
@@ -791,7 +1054,7 @@
 
     header.SetQuestionCount(kQuestionCount[aInfo.mQueryType]);
 
-    message = mSocket.NewMessage(0);
+    message = mSocket.NewMessage();
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = message->Append(header));
@@ -804,64 +1067,106 @@
         SuccessOrExit(error = message->Append(Question(kQuestionRecordTypes[aInfo.mQueryType][num])));
     }
 
-    messageInfo.SetPeerAddr(aInfo.mConfig.GetServerSockAddr().GetAddress());
-    messageInfo.SetPeerPort(aInfo.mConfig.GetServerSockAddr().GetPort());
+    length = message->GetLength() - message->GetOffset();
 
-    SuccessOrExit(error = mSocket.SendTo(*message, messageInfo));
+    if (aInfo.mConfig.GetTransportProto() == QueryConfig::kDnsTransportTcp)
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+    {
+        // Check if query will fit into tcp buffer if not return error.
+        VerifyOrExit(length + sizeof(uint16_t) + mSendLink.mLength <=
+                         OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_QUERY_MAX_SIZE,
+                     error = kErrorNoBufs);
+
+        // In case of initialized connection check if connected peer and new query have the same address.
+        if (mTcpState != kTcpUninitialized)
+        {
+            VerifyOrExit(mEndpoint.GetPeerAddress() == AsCoreType(&aInfo.mConfig.mServerSockAddr),
+                         error = kErrorFailed);
+        }
+
+        switch (mTcpState)
+        {
+        case kTcpUninitialized:
+            SuccessOrExit(error = InitTcpSocket());
+            SuccessOrExit(
+                error = mEndpoint.Connect(AsCoreType(&aInfo.mConfig.mServerSockAddr), OT_TCP_CONNECT_NO_FAST_OPEN));
+            mTcpState = kTcpConnecting;
+            PrepareTcpMessage(*message);
+            break;
+        case kTcpConnectedIdle:
+            PrepareTcpMessage(*message);
+            SuccessOrExit(error = mEndpoint.SendByReference(mSendLink, /* aFlags */ 0));
+            mTcpState = kTcpConnectedSending;
+            break;
+        case kTcpConnecting:
+            PrepareTcpMessage(*message);
+            break;
+        case kTcpConnectedSending:
+            WriteUint16(length, mSendBufferBytes + mSendLink.mLength);
+            SuccessOrAssert(error = message->Read(message->GetOffset(),
+                                                  (mSendBufferBytes + sizeof(uint16_t) + mSendLink.mLength), length));
+            IgnoreError(mEndpoint.SendByExtension(length + sizeof(uint16_t), /* aFlags */ 0));
+            break;
+        }
+        message->Free();
+        message = nullptr;
+    }
+#else
+    {
+        error = kErrorInvalidArgs;
+        LogWarn("DNS query over TCP not supported.");
+        ExitNow();
+    }
+#endif
+    else
+    {
+        VerifyOrExit(length <= kUdpQueryMaxSize, error = kErrorInvalidArgs);
+        messageInfo.SetPeerAddr(aInfo.mConfig.GetServerSockAddr().GetAddress());
+        messageInfo.SetPeerPort(aInfo.mConfig.GetServerSockAddr().GetPort());
+        SuccessOrExit(error = mSocket.SendTo(*message, messageInfo));
+    }
 
 exit:
+
     FreeMessageOnError(message, error);
-
-    UpdateQuery(aQuery, aInfo);
-
     if (aUpdateTimer)
     {
         mTimer.FireAtIfEarlier(aInfo.mRetransmissionTime);
     }
+
+    UpdateQuery(aQuery, aInfo);
+
+    return error;
 }
 
 Error Client::AppendNameFromQuery(const Query &aQuery, Message &aMessage)
 {
-    Error    error = kErrorNone;
-    uint16_t offset;
-    uint16_t length;
+    // The name is encoded and included after the `Info` in `aQuery`
+    // starting at `kNameOffsetInQuery`.
 
-    // The name is encoded and included after the `Info` in `aQuery`. We
-    // first calculate the encoded length of the name, then grow the
-    // message, and finally copy the encoded name bytes from `aQuery`
-    // into `aMessage`.
-
-    length = aQuery.GetLength() - kNameOffsetInQuery;
-
-    offset = aMessage.GetLength();
-    SuccessOrExit(error = aMessage.SetLength(offset + length));
-
-    aQuery.CopyTo(/* aSourceOffset */ kNameOffsetInQuery, /* aDestOffset */ offset, length, aMessage);
-
-exit:
-    return error;
+    return aMessage.AppendBytesFromMessage(aQuery, kNameOffsetInQuery, aQuery.GetLength() - kNameOffsetInQuery);
 }
 
 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
@@ -881,44 +1186,53 @@
         }
         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;
 }
 
 Client::Query *Client::FindQueryById(uint16_t aMessageId)
 {
-    Query *   matchedQuery = nullptr;
+    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;
 }
 
@@ -929,58 +1243,78 @@
     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 (!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);
         }
     }
@@ -992,79 +1326,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
-
-    if (aType == kIp6AddressQuery)
-    {
-        Ip6::Address ip6ddress;
-        uint32_t     ttl;
-        ARecord      aRecord;
-
-        // 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)
-        {
-            aResponse.mIp6QueryResponseRequiresNat64 = true;
-            aResponseError                           = kErrorNone;
-            ExitNow();
-        }
-
-        // 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;
-
-        SendQuery(*aResponse.mQuery, info, /* aUpdateTimer */ true);
-
-        error = kErrorPending;
-    }
-
-#endif // OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
-
 exit:
-    if (error != kErrorNone)
-    {
-        LogInfo("Failed to parse response %s", ErrorToString(error));
-    }
-
     return error;
 }
 
-void Client::HandleTimer(Timer &aTimer)
+bool Client::CanFinalizeQuery(Query &aQuery)
 {
-    aTimer.Get<Client>().HandleTimer();
+    // 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)
+    {
+        info.ReadFrom(*query);
+
+        if (query == &aQuery)
+        {
+            continue;
+        }
+
+        if (info.mSavedResponse == nullptr)
+        {
+            canFinalize = false;
+            ExitNow();
+        }
+    }
+
+exit:
+    return canFinalize;
+}
+
+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)
@@ -1072,25 +1430,43 @@
     TimeMilli now      = TimerMilli::GetNow();
     TimeMilli nextTime = now.GetDistantFuture();
     QueryInfo info;
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+    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;
             }
 
-            SendQuery(query, info, /* aUpdateTimer */ false);
-        }
+            if (now >= info.mRetransmissionTime)
+            {
+                if (info.mTransmissionCount >= info.mConfig.GetMaxTxAttempts())
+                {
+                    FinalizeQuery(*query, kErrorResponseTimeout);
+                    break;
+                }
 
-        if (nextTime > info.mRetransmissionTime)
-        {
-            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;
+            }
+#endif
         }
     }
 
@@ -1098,8 +1474,260 @@
     {
         mTimer.FireAt(nextTime);
     }
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+    if (!hasTcpQuery && mTcpState != kTcpUninitialized)
+    {
+        IgnoreError(mEndpoint.SendEndOfStream());
+    }
+#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;
+}
+
+#endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+void Client::PrepareTcpMessage(Message &aMessage)
+{
+    uint16_t length = aMessage.GetLength() - aMessage.GetOffset();
+
+    // Prepending the DNS query with length of the packet according to RFC1035.
+    WriteUint16(length, mSendBufferBytes + mSendLink.mLength);
+    SuccessOrAssert(
+        aMessage.Read(aMessage.GetOffset(), (mSendBufferBytes + sizeof(uint16_t) + mSendLink.mLength), length));
+    mSendLink.mLength += length + sizeof(uint16_t);
+}
+
+void Client::HandleTcpSendDone(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData)
+{
+    OT_UNUSED_VARIABLE(aEndpoint);
+    OT_UNUSED_VARIABLE(aData);
+    OT_ASSERT(mTcpState == kTcpConnectedSending);
+
+    mSendLink.mLength = 0;
+    mTcpState         = kTcpConnectedIdle;
+}
+
+void Client::HandleTcpSendDoneCallback(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData)
+{
+    static_cast<Client *>(otTcpEndpointGetContext(aEndpoint))->HandleTcpSendDone(aEndpoint, aData);
+}
+
+void Client::HandleTcpEstablished(otTcpEndpoint *aEndpoint)
+{
+    OT_UNUSED_VARIABLE(aEndpoint);
+    IgnoreError(mEndpoint.SendByReference(mSendLink, /* aFlags */ 0));
+    mTcpState = kTcpConnectedSending;
+}
+
+void Client::HandleTcpEstablishedCallback(otTcpEndpoint *aEndpoint)
+{
+    static_cast<Client *>(otTcpEndpointGetContext(aEndpoint))->HandleTcpEstablished(aEndpoint);
+}
+
+Error Client::ReadFromLinkBuffer(const otLinkedBuffer *&aLinkedBuffer,
+                                 size_t                &aOffset,
+                                 Message               &aMessage,
+                                 uint16_t               aLength)
+{
+    // Read `aLength` bytes from `aLinkedBuffer` starting at `aOffset`
+    // and copy the content into `aMessage`. As we read we can move
+    // to the next `aLinkedBuffer` and update `aOffset`.
+    // Returns:
+    // - `kErrorNone` if `aLength` bytes are successfully read and
+    //    `aOffset` and `aLinkedBuffer` are updated.
+    // - `kErrorNotFound` is not enough bytes available to read
+    //    from `aLinkedBuffer`.
+    // - `kErrorNotBufs` if cannot grow `aMessage` to append bytes.
+
+    Error error = kErrorNone;
+
+    while (aLength > 0)
+    {
+        uint16_t bytesToRead = aLength;
+
+        VerifyOrExit(aLinkedBuffer != nullptr, error = kErrorNotFound);
+
+        if (bytesToRead > aLinkedBuffer->mLength - aOffset)
+        {
+            bytesToRead = static_cast<uint16_t>(aLinkedBuffer->mLength - aOffset);
+        }
+
+        SuccessOrExit(error = aMessage.AppendBytes(&aLinkedBuffer->mData[aOffset], bytesToRead));
+
+        aLength -= bytesToRead;
+        aOffset += bytesToRead;
+
+        if (aOffset == aLinkedBuffer->mLength)
+        {
+            aLinkedBuffer = aLinkedBuffer->mNext;
+            aOffset       = 0;
+        }
+    }
+
+exit:
+    return error;
+}
+
+void Client::HandleTcpReceiveAvailable(otTcpEndpoint *aEndpoint,
+                                       size_t         aBytesAvailable,
+                                       bool           aEndOfStream,
+                                       size_t         aBytesRemaining)
+{
+    OT_UNUSED_VARIABLE(aEndpoint);
+    OT_UNUSED_VARIABLE(aBytesRemaining);
+
+    Message              *message   = nullptr;
+    size_t                totalRead = 0;
+    size_t                offset    = 0;
+    const otLinkedBuffer *data;
+
+    if (aEndOfStream)
+    {
+        // Cleanup is done in disconnected callback.
+        IgnoreError(mEndpoint.SendEndOfStream());
+    }
+
+    SuccessOrExit(mEndpoint.ReceiveByReference(data));
+    VerifyOrExit(data != nullptr);
+
+    message = mSocket.NewMessage();
+    VerifyOrExit(message != nullptr);
+
+    while (aBytesAvailable > totalRead)
+    {
+        uint16_t length;
+
+        // Read the `length` field.
+        SuccessOrExit(ReadFromLinkBuffer(data, offset, *message, sizeof(uint16_t)));
+
+        IgnoreError(message->Read(/* aOffset */ 0, length));
+        length = HostSwap16(length);
+
+        // Try to read `length` bytes.
+        IgnoreError(message->SetLength(0));
+        SuccessOrExit(ReadFromLinkBuffer(data, offset, *message, length));
+
+        totalRead += length + sizeof(uint16_t);
+
+        // Now process the read message as query response.
+        ProcessResponse(*message);
+
+        IgnoreError(message->SetLength(0));
+
+        // Loop again to see if we can read another response.
+    }
+
+exit:
+    // Inform `mEndPoint` about the total read and processed bytes
+    IgnoreError(mEndpoint.CommitReceive(totalRead, /* aFlags */ 0));
+    FreeMessage(message);
+}
+
+void Client::HandleTcpReceiveAvailableCallback(otTcpEndpoint *aEndpoint,
+                                               size_t         aBytesAvailable,
+                                               bool           aEndOfStream,
+                                               size_t         aBytesRemaining)
+{
+    static_cast<Client *>(otTcpEndpointGetContext(aEndpoint))
+        ->HandleTcpReceiveAvailable(aEndpoint, aBytesAvailable, aEndOfStream, aBytesRemaining);
+}
+
+void Client::HandleTcpDisconnected(otTcpEndpoint *aEndpoint, otTcpDisconnectedReason aReason)
+{
+    OT_UNUSED_VARIABLE(aEndpoint);
+    OT_UNUSED_VARIABLE(aReason);
+    QueryInfo info;
+
+    IgnoreError(mEndpoint.Deinitialize());
+    mTcpState = kTcpUninitialized;
+
+    // Abort queries in case of connection failures
+    for (Query &mainQuery : mMainQueries)
+    {
+        info.ReadFrom(mainQuery);
+
+        if (info.mConfig.GetTransportProto() == QueryConfig::kDnsTransportTcp)
+        {
+            FinalizeQuery(mainQuery, kErrorAbort);
+        }
+    }
+}
+
+void Client::HandleTcpDisconnectedCallback(otTcpEndpoint *aEndpoint, otTcpDisconnectedReason aReason)
+{
+    static_cast<Client *>(otTcpEndpointGetContext(aEndpoint))->HandleTcpDisconnected(aEndpoint, aReason);
+}
+
+#endif // OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+
 } // namespace Dns
 } // namespace ot
 
diff --git a/src/core/net/dns_client.hpp b/src/core/net/dns_client.hpp
index de9afca..12c63b9 100644
--- a/src/core/net/dns_client.hpp
+++ b/src/core/net/dns_client.hpp
@@ -61,6 +61,10 @@
 
 #endif
 
+#if !OPENTHREAD_CONFIG_TCP_ENABLE && OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+#error "OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE requires OPENTHREAD_CONFIG_TCP_ENABLE"
+#endif
+
 /**
  * This struct represents an opaque (and empty) type for a response to an address resolution DNS query.
  *
@@ -136,12 +140,37 @@
         enum Nat64Mode : uint8_t
         {
             kNat64Unspecified = OT_DNS_NAT64_UNSPECIFIED, ///< NAT64 mode is not specified. Use default NAT64 mode.
-            kNat64Allow       = OT_DNS_NAT64_ALLOW,       ///< Allow NAT64 address translation
+            kNat64Allow       = OT_DNS_NAT64_ALLOW,       ///< Allow NAT64 address translation.
             kNat64Disallow    = OT_DNS_NAT64_DISALLOW,    ///< Disallow NAT64 address translation.
         };
 #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.
+         *
+         */
+        enum TransportProto : uint8_t
+        {
+            kDnsTransportUnspecified = OT_DNS_TRANSPORT_UNSPECIFIED, /// Dns transport is unspecified.
+            kDnsTransportUdp         = OT_DNS_TRANSPORT_UDP,         /// Dns query should be sent via UDP.
+            kDnsTransportTcp         = OT_DNS_TRANSPORT_TCP,         /// Dns query should be sent via TCP.
+        };
+
+        /**
          * This is the default constructor for `QueryConfig` object.
          *
          */
@@ -191,12 +220,31 @@
          */
         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.
+         *
+         * @returns The transport protocol.
+         *
+         */
+        TransportProto GetTransportProto(void) const { return static_cast<TransportProto>(mTransportProto); };
 
     private:
         static constexpr uint32_t kDefaultResponseTimeout = OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_RESPONSE_TIMEOUT;
         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;
@@ -216,11 +264,16 @@
         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
+        void SetTransportProto(TransportProto aTransportProto)
+        {
+            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
@@ -257,21 +310,25 @@
         void  SelectSection(Section aSection, uint16_t &aOffset, uint16_t &aNumRecord) const;
         Error CheckForHostNameAlias(Section aSection, Name &aHostName) const;
         Error FindHostAddress(Section       aSection,
-                              const Name &  aHostName,
+                              const Name   &aHostName,
                               uint16_t      aIndex,
                               Ip6::Address &aAddress,
-                              uint32_t &    aTtl) const;
+                              uint32_t     &aTtl) const;
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
         Error FindARecord(Section aSection, const Name &aHostName, uint16_t aIndex, ARecord &aARecord) const;
 #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.
+        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`.
@@ -486,9 +543,9 @@
          * @retval kErrorNoBufs  Either the label or name does not fit in the given buffers.
          *
          */
-        Error GetServiceName(char *   aLabelBuffer,
+        Error GetServiceName(char    *aLabelBuffer,
                              uint8_t  aLabelBufferSize,
-                             char *   aNameBuffer,
+                             char    *aNameBuffer,
                              uint16_t aNameBufferSize) const;
 
         /**
@@ -604,9 +661,9 @@
      * @retval kErrorInvalidState   Cannot send query since Thread interface is not up.
      *
      */
-    Error ResolveAddress(const char *       aHostName,
+    Error ResolveAddress(const char        *aHostName,
                          AddressCallback    aCallback,
-                         void *             aContext,
+                         void              *aContext,
                          const QueryConfig *aConfig = nullptr);
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
@@ -631,9 +688,9 @@
      * @retval kErrorInvalidState   Cannot send query since Thread interface is not up, or there is no NAT64 prefix.
      *
      */
-    Error ResolveIp4Address(const char *       aHostName,
+    Error ResolveIp4Address(const char        *aHostName,
                             AddressCallback    aCallback,
-                            void *             aContext,
+                            void              *aContext,
                             const QueryConfig *aConfig = nullptr);
 #endif
 
@@ -655,9 +712,9 @@
      * @retval kErrorNoBufs     Insufficient buffer to prepare and send query.
      *
      */
-    Error Browse(const char *       aServiceName,
+    Error Browse(const char        *aServiceName,
                  BrowseCallback     aCallback,
-                 void *             aContext,
+                 void              *aContext,
                  const QueryConfig *aConfig = nullptr);
 
     /**
@@ -678,11 +735,11 @@
      * @retval kErrorInvalidArgs  @p aInstanceLabel is `nullptr`.
      *
      */
-    Error ResolveService(const char *         aInstanceLabel,
-                         const char *         aServiceName,
+    Error ResolveService(const char          *aInstanceLabel,
+                         const char          *aServiceName,
                          otDnsServiceCallback aCallback,
-                         void *               aContext,
-                         const QueryConfig *  aConfig = nullptr);
+                         void                *aContext,
+                         const QueryConfig   *aConfig = nullptr);
 
 #endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
 
@@ -694,11 +751,24 @@
         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,
+        kTcpConnecting,
+        kTcpConnectedIdle,
+        kTcpConnectedSending,
+    };
+#endif
+
     union Callback
     {
         AddressCallback mAddressCallback;
@@ -717,43 +787,75 @@
         QueryType   mQueryType;
         uint16_t    mMessageId;
         Callback    mCallback;
-        void *      mCallbackContext;
+        void       *mCallbackContext;
         TimeMilli   mRetransmissionTime;
         QueryConfig mConfig;
         uint8_t     mTransmissionCount;
+        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); }
-    void        SendQuery(Query &aQuery, QueryInfo &aInfo, bool aUpdateTimer);
+    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);
+    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);
-    static void HandleTimer(Timer &aTimer);
+    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 CheckAddressResponse(Response &aResponse, Error aResponseError) const;
+    Error ReplaceWithIp4Query(Query &aQuery);
 #endif
+#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+    Error ReplaceWithSeparateSrvTxtQueries(Query &aQuery);
+#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[];
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+    static void HandleTcpEstablishedCallback(otTcpEndpoint *aEndpoint);
+    static void HandleTcpSendDoneCallback(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData);
+    static void HandleTcpDisconnectedCallback(otTcpEndpoint *aEndpoint, otTcpDisconnectedReason aReason);
+    static void HandleTcpReceiveAvailableCallback(otTcpEndpoint *aEndpoint,
+                                                  size_t         aBytesAvailable,
+                                                  bool           aEndOfStream,
+                                                  size_t         aBytesRemaining);
+
+    void  HandleTcpEstablished(otTcpEndpoint *aEndpoint);
+    void  HandleTcpSendDone(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData);
+    void  HandleTcpDisconnected(otTcpEndpoint *aEndpoint, otTcpDisconnectedReason aReason);
+    void  HandleTcpReceiveAvailable(otTcpEndpoint *aEndpoint,
+                                    size_t         aBytesAvailable,
+                                    bool           aEndOfStream,
+                                    size_t         aBytesRemaining);
+    Error InitTcpSocket(void);
+    Error ReadFromLinkBuffer(const otLinkedBuffer *&aLinkedBuffer,
+                             size_t                &aOffset,
+                             Message               &aMessage,
+                             uint16_t               aLength);
+    void  PrepareTcpMessage(Message &aMessage);
+#endif // OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+
+    static const uint8_t         kQuestionCount[];
+    static const uint16_t *const kQuestionRecordTypes[];
 
     static const uint16_t kIp6AddressQueryRecordTypes[];
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
@@ -764,10 +866,25 @@
     static const uint16_t kServiceQueryRecordTypes[];
 #endif
 
+    static constexpr uint16_t kUdpQueryMaxSize = 512;
+
+    using RetryTimer = TimerMilliIn<Client, &Client::HandleTimer>;
+
     Ip6::Udp::Socket mSocket;
-    QueryList        mQueries;
-    TimerMilli       mTimer;
-    QueryConfig      mDefaultConfig;
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+    Ip6::Tcp::Endpoint mEndpoint;
+
+    otLinkedBuffer mSendLink;
+    uint8_t        mSendBufferBytes[OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_QUERY_MAX_SIZE];
+    uint8_t        mReceiveBufferBytes[OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_QUERY_MAX_SIZE];
+
+    TcpState mTcpState;
+#endif
+
+    QueryList   mMainQueries;
+    RetryTimer  mTimer;
+    QueryConfig mDefaultConfig;
 #if OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE
     bool mUserDidSetDefaultAddress;
 #endif
diff --git a/src/core/net/dns_dso.cpp b/src/core/net/dns_dso.cpp
index a18b459..82ee291 100644
--- a/src/core/net/dns_dso.cpp
+++ b/src/core/net/dns_dso.cpp
@@ -37,6 +37,7 @@
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 
 /**
@@ -80,9 +81,9 @@
 //---------------------------------------------------------------------------------------------------------------------
 // Dso::Connection
 
-Dso::Connection::Connection(Instance &           aInstance,
+Dso::Connection::Connection(Instance            &aInstance,
                             const Ip6::SockAddr &aPeerSockAddr,
-                            Callbacks &          aCallbacks,
+                            Callbacks           &aCallbacks,
                             uint32_t             aInactivityTimeout,
                             uint32_t             aKeepAliveInterval)
     : InstanceLocator(aInstance)
@@ -321,7 +322,7 @@
 Error Dso::Connection::SendRetryDelayMessage(uint32_t aDelay, Dns::Header::Response aResponseCode)
 {
     Error         error   = kErrorNone;
-    Message *     message = nullptr;
+    Message      *message = nullptr;
     RetryDelayTlv retryDelayTlv;
     MessageId     messageId;
 
@@ -411,7 +412,7 @@
     // `kResponseMessage`.
 
     Error        error   = kErrorNone;
-    Message *    message = nullptr;
+    Message     *message = nullptr;
     KeepAliveTlv keepAliveTlv;
 
     switch (mState)
@@ -479,9 +480,9 @@
     return error;
 }
 
-Error Dso::Connection::SendMessage(Message &             aMessage,
+Error Dso::Connection::SendMessage(Message              &aMessage,
                                    MessageType           aMessageType,
-                                   MessageId &           aMessageId,
+                                   MessageId            &aMessageId,
                                    Dns::Header::Response aResponseCode,
                                    uint32_t              aResponseTimeout)
 {
@@ -787,7 +788,7 @@
 }
 
 Error Dso::Connection::ProcessRequestOrUnidirectionalMessage(const Dns::Header &aHeader,
-                                                             const Message &    aMessage,
+                                                             const Message     &aMessage,
                                                              Tlv::Type          aPrimaryTlvType)
 {
     Error error = kErrorAbort;
@@ -846,7 +847,7 @@
 }
 
 Error Dso::Connection::ProcessResponseMessage(const Dns::Header &aHeader,
-                                              const Message &    aMessage,
+                                              const Message     &aMessage,
                                               Tlv::Type          aPrimaryTlvType)
 {
     Error     error = kErrorAbort;
@@ -1029,7 +1030,7 @@
 
 void Dso::Connection::SendErrorResponse(const Dns::Header &aHeader, Dns::Header::Response aResponseCode)
 {
-    Message *   response = NewMessage();
+    Message    *response = NewMessage();
     Dns::Header header;
 
     VerifyOrExit(response != nullptr);
@@ -1125,7 +1126,7 @@
             // five seconds or one quarter of the new inactivity
             // timeout, whichever is greater [RFC 8490 - 7.1.1].
 
-            newExpiration = now + OT_MAX(kMinServerInactivityWaitTime, aNewTimeout / 4);
+            newExpiration = now + Max(kMinServerInactivityWaitTime, aNewTimeout / 4);
         }
     }
 
@@ -1143,7 +1144,7 @@
 
     OT_ASSERT(mInactivity.IsUsed());
 
-    return OT_MAX(mInactivity.GetInterval() * 2, kMinServerInactivityWaitTime);
+    return Max(mInactivity.GetInterval() * 2, kMinServerInactivityWaitTime);
 }
 
 void Dso::Connection::ResetTimeouts(bool aIsKeepAliveMessage)
@@ -1219,12 +1220,12 @@
     case kStateConnectedButSessionless:
     case kStateEstablishingSession:
     case kStateSessionEstablished:
-        nextTime = OT_MIN(nextTime, mPendingRequests.GetNextFireTime(aNow));
+        nextTime = Min(nextTime, mPendingRequests.GetNextFireTime(aNow));
 
         if (mKeepAlive.IsUsed())
         {
             VerifyOrExit(mKeepAlive.GetExpirationTime() > aNow, nextTime = aNow);
-            nextTime = OT_MIN(nextTime, mKeepAlive.GetExpirationTime());
+            nextTime = Min(nextTime, mKeepAlive.GetExpirationTime());
         }
 
         if (mInactivity.IsUsed() && mPendingRequests.IsEmpty() && !mLongLivedOperation)
@@ -1234,7 +1235,7 @@
             // active long-lived operation.
 
             VerifyOrExit(mInactivity.GetExpirationTime() > aNow, nextTime = aNow);
-            nextTime = OT_MIN(nextTime, mInactivity.GetExpirationTime());
+            nextTime = Min(nextTime, mInactivity.GetExpirationTime());
         }
 
         break;
@@ -1311,7 +1312,7 @@
     }
 
 exit:
-    aNextTime = OT_MIN(aNextTime, GetNextFireTime(aNow));
+    aNextTime = Min(aNextTime, GetNextFireTime(aNow));
     SignalAnyStateChange();
 }
 
@@ -1407,10 +1408,7 @@
     return error;
 }
 
-void Dso::Connection::PendingRequests::Remove(MessageId aMessageId)
-{
-    mRequests.RemoveMatching(aMessageId);
-}
+void Dso::Connection::PendingRequests::Remove(MessageId aMessageId) { mRequests.RemoveMatching(aMessageId); }
 
 bool Dso::Connection::PendingRequests::HasAnyTimedOut(TimeMilli aNow) const
 {
@@ -1435,7 +1433,7 @@
     for (const Entry &entry : mRequests)
     {
         VerifyOrExit(entry.mTimeout > aNow, nextTime = aNow);
-        nextTime = OT_MIN(entry.mTimeout, nextTime);
+        nextTime = Min(entry.mTimeout, nextTime);
     }
 
 exit:
@@ -1448,7 +1446,7 @@
 Dso::Dso(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mAcceptHandler(nullptr)
-    , mTimer(aInstance, HandleTimer)
+    , mTimer(aInstance)
 {
 }
 
@@ -1458,10 +1456,7 @@
     otPlatDsoEnableListening(&GetInstance(), true);
 }
 
-void Dso::StopListening(void)
-{
-    otPlatDsoEnableListening(&GetInstance(), false);
-}
+void Dso::StopListening(void) { otPlatDsoEnableListening(&GetInstance(), false); }
 
 Dso::Connection *Dso::FindClientConnection(const Ip6::SockAddr &aPeerSockAddr)
 {
@@ -1487,11 +1482,6 @@
     return connection;
 }
 
-void Dso::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Dso>().HandleTimer();
-}
-
 void Dso::HandleTimer(void)
 {
     TimeMilli   now      = TimerMilli::GetNow();
diff --git a/src/core/net/dns_dso.hpp b/src/core/net/dns_dso.hpp
index d9ff458..6ab6800 100644
--- a/src/core/net/dns_dso.hpp
+++ b/src/core/net/dns_dso.hpp
@@ -43,6 +43,7 @@
 #include "common/locator.hpp"
 #include "common/message.hpp"
 #include "common/non_copyable.hpp"
+#include "common/num_utils.hpp"
 #include "common/timer.hpp"
 #include "net/dns_types.hpp"
 #include "net/socket.hpp"
@@ -305,7 +306,7 @@
              * @retval kErrorAbort     Fatal error (misbehavior by peer). This triggers aborting of the connection.
              *
              */
-            typedef Error (&ProcessRequestMessage)(Connection &   aConnection,
+            typedef Error (&ProcessRequestMessage)(Connection    &aConnection,
                                                    MessageId      aMessageId,
                                                    const Message &aMessage,
                                                    Tlv::Type      aPrimaryTlvType);
@@ -328,7 +329,7 @@
              *                        @p aPrimaryTlvType is not known in a unidirectional message, it is a fatal error.
              *
              */
-            typedef Error (&ProcessUnidirectionalMessage)(Connection &   aConnection,
+            typedef Error (&ProcessUnidirectionalMessage)(Connection    &aConnection,
                                                           const Message &aMessage,
                                                           Tlv::Type      aPrimaryTlvType);
 
@@ -358,9 +359,9 @@
              * @retval kErrorAbort    Fatal error (misbehavior by peer). This triggers aborting of the connection.
              *
              */
-            typedef Error (&ProcessResponseMessage)(Connection &       aConnection,
+            typedef Error (&ProcessResponseMessage)(Connection        &aConnection,
                                                     const Dns::Header &aHeader,
-                                                    const Message &    aMessage,
+                                                    const Message     &aMessage,
                                                     Tlv::Type          aResponseTlvType,
                                                     Tlv::Type          aRequestTlvType);
             /**
@@ -411,9 +412,9 @@
          * @param[in] aKeepAliveInterval  The Keep Alive timeout interval (in msec).
          *
          */
-        Connection(Instance &           aInstance,
+        Connection(Instance            &aInstance,
                    const Ip6::SockAddr &aPeerSockAddr,
-                   Callbacks &          aCallbacks,
+                   Callbacks           &aCallbacks,
                    uint32_t             aInactivityTimeout = kDefaultTimeout,
                    uint32_t             aKeepAliveInterval = kDefaultTimeout);
 
@@ -547,7 +548,7 @@
          * @retval  kErrorNoBufs    Failed to allocate new buffer to prepare the message (append header or padding).
          *
          */
-        Error SendRequestMessage(Message &  aMessage,
+        Error SendRequestMessage(Message   &aMessage,
                                  MessageId &aMessageId,
                                  uint32_t   aResponseTimeout = kResponseTimeout);
 
@@ -773,7 +774,7 @@
                 // If it is not infinite, limit the interval to `kMaxInterval`.
                 // The max limit ensures that even twice the interval is less
                 // than max OpenThread timer duration.
-                return (aInterval == kInfinite) ? aInterval : OT_MIN(aInterval, kMaxInterval);
+                return (aInterval == kInfinite) ? aInterval : Min(aInterval, kMaxInterval);
             }
 
             uint32_t  mInterval;
@@ -791,15 +792,15 @@
         void MarkAsDisconnected(void);
 
         Error SendKeepAliveMessage(MessageType aMessageType, MessageId aResponseId);
-        Error SendMessage(Message &             aMessage,
+        Error SendMessage(Message              &aMessage,
                           MessageType           aMessageType,
-                          MessageId &           aMessageId,
+                          MessageId            &aMessageId,
                           Dns::Header::Response aResponseCode    = Dns::Header::kResponseSuccess,
                           uint32_t              aResponseTimeout = kResponseTimeout);
         void  HandleReceive(Message &aMessage);
         Error ReadPrimaryTlv(const Message &aMessage, Tlv::Type &aPrimaryTlvType) const;
         Error ProcessRequestOrUnidirectionalMessage(const Dns::Header &aHeader,
-                                                    const Message &    aMessage,
+                                                    const Message     &aMessage,
                                                     Tlv::Type          aPrimaryTlvType);
         Error ProcessResponseMessage(const Dns::Header &aHeader, const Message &aMessage, Tlv::Type aPrimaryTlvType);
         Error ProcessKeepAliveMessage(const Dns::Header &aHeader, const Message &aMessage);
@@ -819,8 +820,8 @@
         static const char *MessageTypeToString(MessageType aMessageType);
         static const char *DisconnectReasonToString(DisconnectReason aReason);
 
-        Connection *          mNext;
-        Callbacks &           mCallbacks;
+        Connection           *mNext;
+        Callbacks            &mCallbacks;
         Ip6::SockAddr         mPeerSockAddr;
         State                 mState;
         MessageId             mNextMessageId;
@@ -952,13 +953,14 @@
 
     Connection *AcceptConnection(const Ip6::SockAddr &aPeerSockAddr);
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTimer(void);
+
+    using DsoTimer = TimerMilliIn<Dso, &Dso::HandleTimer>;
 
     AcceptHandler          mAcceptHandler;
     LinkedList<Connection> mClientConnections;
     LinkedList<Connection> mServerConnections;
-    TimerMilli             mTimer;
+    DsoTimer               mTimer;
 };
 
 } // namespace Dns
diff --git a/examples/platforms/cc2538/system.c b/src/core/net/dns_platform.cpp
similarity index 68%
rename from examples/platforms/cc2538/system.c
rename to src/core/net/dns_platform.cpp
index 0d1cd63..ef48077 100644
--- a/examples/platforms/cc2538/system.c
+++ b/src/core/net/dns_platform.cpp
@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2016, The OpenThread Authors.
+ *  Copyright (c) 2023, The OpenThread Authors.
  *  All rights reserved.
  *
  *  Redistribution and use in source and binary forms, with or without
@@ -28,39 +28,25 @@
 
 /**
  * @file
- * @brief
- *   This file includes the platform-specific initializers.
+ *   This file implements the DNS platform callbacks into OpenThread.
  */
-#include "platform-cc2538.h"
-#include <openthread/config.h>
 
-otInstance *sInstance;
+#include "openthread-core-config.h"
 
-void otSysInit(int argc, char *argv[])
+#include <openthread/instance.h>
+#include <openthread/platform/dns.h>
+
+#include "common/code_utils.hpp"
+#include "common/instance.hpp"
+#include "common/message.hpp"
+
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+
+using namespace ot;
+
+void otPlatDnsUpstreamQueryDone(otInstance *aInstance, otPlatDnsUpstreamQuery *aTxn, otMessage *aResponse)
 {
-    OT_UNUSED_VARIABLE(argc);
-    OT_UNUSED_VARIABLE(argv);
-
-#if OPENTHREAD_CONFIG_ENABLE_DEBUG_UART
-    cc2538DebugUartInit();
+    return AsCoreType(aInstance).Get<Dns::ServiceDiscovery::Server>().OnUpstreamQueryDone(AsCoreType(aTxn),
+                                                                                          AsCoreTypePtr(aResponse));
+}
 #endif
-    cc2538AlarmInit();
-    cc2538RandomInit();
-    cc2538RadioInit();
-}
-
-bool otSysPseudoResetWasRequested(void)
-{
-    return false;
-}
-
-void otSysProcessDrivers(otInstance *aInstance)
-{
-    sInstance = aInstance;
-
-    // should sleep and wait for interrupts here
-
-    cc2538UartProcess();
-    cc2538RadioProcess(aInstance);
-    cc2538AlarmProcess(aInstance);
-}
diff --git a/src/core/net/dns_types.cpp b/src/core/net/dns_types.cpp
index e65cd03..7c3241e 100644
--- a/src/core/net/dns_types.cpp
+++ b/src/core/net/dns_types.cpp
@@ -36,6 +36,7 @@
 #include "common/code_utils.hpp"
 #include "common/debug.hpp"
 #include "common/instance.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 #include "common/string.hpp"
 
@@ -182,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);
 
@@ -327,14 +328,14 @@
 
             if (!firstLabel)
             {
-                *aNameBuffer++ = kLabelSeperatorChar;
+                *aNameBuffer++ = kLabelSeparatorChar;
                 aNameBufferSize--;
 
                 // No need to check if we have reached end of the name buffer
                 // here since `iterator.ReadLabel()` would verify it.
             }
 
-            labelLength = static_cast<uint8_t>(OT_MIN(static_cast<uint8_t>(kMaxLabelSize), aNameBufferSize));
+            labelLength = static_cast<uint8_t>(Min(static_cast<uint16_t>(kMaxLabelSize), aNameBufferSize));
             SuccessOrExit(error = iterator.ReadLabel(aNameBuffer, labelLength, /* aAllowDotCharInLabel */ false));
             aNameBuffer += labelLength;
             aNameBufferSize -= labelLength;
@@ -344,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;
@@ -381,7 +382,7 @@
     LabelIterator iterator(aMessage, aOffset);
     bool          matches = true;
 
-    if (*aName == kLabelSeperatorChar)
+    if (*aName == kLabelSeparatorChar)
     {
         aName++;
         VerifyOrExit(*aName == kNullChar, error = kErrorInvalidArgs);
@@ -521,6 +522,7 @@
             // specify an offset value from the start of the DNS header.
 
             uint16_t pointerValue;
+            uint16_t nextLabelOffset;
 
             SuccessOrExit(error = mMessage.Read(mNextLabelOffset, pointerValue));
 
@@ -531,7 +533,9 @@
 
             // `mMessage.GetOffset()` must point to the start of the
             // DNS header.
-            mNextLabelOffset = mMessage.GetOffset() + (HostSwap16(pointerValue) & kPointerLabelOffsetMask);
+            nextLabelOffset = mMessage.GetOffset() + (HostSwap16(pointerValue) & kPointerLabelOffsetMask);
+            VerifyOrExit(nextLabelOffset < mNextLabelOffset, error = kErrorParse);
+            mNextLabelOffset = nextLabelOffset;
 
             // Go back through the `while(true)` loop to get the next label.
         }
@@ -557,7 +561,7 @@
 
     if (!aAllowDotCharInLabel)
     {
-        VerifyOrExit(StringFind(aLabelBuffer, kLabelSeperatorChar) == nullptr, error = kErrorParse);
+        VerifyOrExit(StringFind(aLabelBuffer, kLabelSeparatorChar) == nullptr, error = kErrorParse);
     }
 
 exit:
@@ -594,7 +598,7 @@
 
     matches = (*aName == kNullChar);
 
-    if (!aIsSingleLabel && (*aName == kLabelSeperatorChar))
+    if (!aIsSingleLabel && (*aName == kLabelSeparatorChar))
     {
         matches = true;
         aName++;
@@ -637,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;
@@ -655,7 +659,7 @@
 
     if (nameLength > domainLength)
     {
-        VerifyOrExit(aName[-1] == kLabelSeperatorChar);
+        VerifyOrExit(aName[-1] == kLabelSeparatorChar);
     }
 
     // This method allows either `aName` or `aDomain` to include or
@@ -683,6 +687,11 @@
     return match;
 }
 
+bool Name::IsSameDomain(const char *aDomain1, const char *aDomain2)
+{
+    return IsSubDomainOf(aDomain1, aDomain2) && IsSubDomainOf(aDomain2, aDomain1);
+}
+
 Error ResourceRecord::ParseRecords(const Message &aMessage, uint16_t &aOffset, uint16_t aNumRecords)
 {
     Error error = kErrorNone;
@@ -735,11 +744,11 @@
     return error;
 }
 
-Error ResourceRecord::FindRecord(const Message & aMessage,
-                                 uint16_t &      aOffset,
+Error ResourceRecord::FindRecord(const Message  &aMessage,
+                                 uint16_t       &aOffset,
                                  uint16_t        aNumRecords,
                                  uint16_t        aIndex,
-                                 const Name &    aName,
+                                 const Name     &aName,
                                  uint16_t        aType,
                                  ResourceRecord &aRecord,
                                  uint16_t        aMinRecordSize)
@@ -794,8 +803,8 @@
     return error;
 }
 
-Error ResourceRecord::ReadRecord(const Message & aMessage,
-                                 uint16_t &      aOffset,
+Error ResourceRecord::ReadRecord(const Message  &aMessage,
+                                 uint16_t       &aOffset,
                                  uint16_t        aType,
                                  ResourceRecord &aRecord,
                                  uint16_t        aMinRecordSize)
@@ -828,9 +837,9 @@
 }
 
 Error ResourceRecord::ReadName(const Message &aMessage,
-                               uint16_t &     aOffset,
+                               uint16_t      &aOffset,
                                uint16_t       aStartOffset,
-                               char *         aNameBuffer,
+                               char          *aNameBuffer,
                                uint16_t       aNameBufferSize,
                                bool           aSkipRecord) const
 {
@@ -918,7 +927,7 @@
     uint8_t     length;
     uint8_t     index;
     const char *cur;
-    char *      keyBuffer = GetKeyBuffer();
+    char       *keyBuffer = GetKeyBuffer();
 
     static_assert(sizeof(mChar) == TxtEntry::kMaxKeyLength + 1, "KeyBuffer cannot fit the max key length");
 
@@ -1070,10 +1079,7 @@
     return GetType() == Dns::ResourceRecord::kTypeAaaa && GetSize() == sizeof(*this);
 }
 
-bool KeyRecord::IsValid(void) const
-{
-    return GetType() == Dns::ResourceRecord::kTypeKey;
-}
+bool KeyRecord::IsValid(void) const { return GetType() == Dns::ResourceRecord::kTypeKey; }
 
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
 void Ecdsa256KeyRecord::Init(void)
@@ -1094,16 +1100,75 @@
     return GetType() == Dns::ResourceRecord::kTypeSig && GetLength() >= sizeof(*this) - sizeof(ResourceRecord);
 }
 
+void LeaseOption::InitAsShortVariant(uint32_t aLeaseInterval)
+{
+    SetOptionCode(kUpdateLease);
+    SetOptionLength(kShortLength);
+    SetLeaseInterval(aLeaseInterval);
+}
+
+void LeaseOption::InitAsLongVariant(uint32_t aLeaseInterval, uint32_t aKeyLeaseInterval)
+{
+    SetOptionCode(kUpdateLease);
+    SetOptionLength(kLongLength);
+    SetLeaseInterval(aLeaseInterval);
+    SetKeyLeaseInterval(aKeyLeaseInterval);
+}
+
 bool LeaseOption::IsValid(void) const
 {
-    return GetLeaseInterval() <= GetKeyLeaseInterval();
+    bool isValid = false;
+
+    VerifyOrExit((GetOptionLength() == kShortLength) || (GetOptionLength() >= kLongLength));
+    isValid = (GetLeaseInterval() <= GetKeyLeaseInterval());
+
+exit:
+    return isValid;
+}
+
+Error LeaseOption::ReadFrom(const Message &aMessage, uint16_t aOffset, uint16_t aLength)
+{
+    Error    error = kErrorNone;
+    uint16_t endOffset;
+
+    VerifyOrExit(static_cast<uint32_t>(aOffset) + aLength <= aMessage.GetLength(), error = kErrorParse);
+
+    endOffset = aOffset + aLength;
+
+    while (aOffset < endOffset)
+    {
+        uint16_t size;
+
+        SuccessOrExit(error = aMessage.Read(aOffset, this, sizeof(Option)));
+
+        VerifyOrExit(aOffset + GetSize() <= endOffset, error = kErrorParse);
+
+        size = static_cast<uint16_t>(GetSize());
+
+        if (GetOptionCode() == kUpdateLease)
+        {
+            VerifyOrExit(GetOptionLength() >= kShortLength, error = kErrorParse);
+
+            IgnoreError(aMessage.Read(aOffset, this, Min(size, static_cast<uint16_t>(sizeof(LeaseOption)))));
+            VerifyOrExit(IsValid(), error = kErrorParse);
+
+            ExitNow();
+        }
+
+        aOffset += size;
+    }
+
+    error = kErrorNotFound;
+
+exit:
+    return error;
 }
 
 Error PtrRecord::ReadPtrName(const Message &aMessage,
-                             uint16_t &     aOffset,
-                             char *         aLabelBuffer,
+                             uint16_t      &aOffset,
+                             char          *aLabelBuffer,
                              uint8_t        aLabelBufferSize,
-                             char *         aNameBuffer,
+                             char          *aNameBuffer,
                              uint16_t       aNameBufferSize) const
 {
     Error    error       = kErrorNone;
@@ -1129,18 +1194,19 @@
 }
 
 Error TxtRecord::ReadTxtData(const Message &aMessage,
-                             uint16_t &     aOffset,
-                             uint8_t *      aTxtBuffer,
-                             uint16_t &     aTxtBufferSize) const
+                             uint16_t      &aOffset,
+                             uint8_t       *aTxtBuffer,
+                             uint16_t      &aTxtBufferSize) const
 {
     Error error = kErrorNone;
 
-    VerifyOrExit(GetLength() <= aTxtBufferSize, error = kErrorNoBufs);
-    SuccessOrExit(error = aMessage.Read(aOffset, aTxtBuffer, GetLength()));
-    VerifyOrExit(VerifyTxtData(aTxtBuffer, GetLength(), /* aAllowEmpty */ true), error = kErrorParse);
-    aTxtBufferSize = GetLength();
+    SuccessOrExit(error = aMessage.Read(aOffset, aTxtBuffer, Min(GetLength(), aTxtBufferSize)));
     aOffset += GetLength();
 
+    VerifyOrExit(GetLength() <= aTxtBufferSize, error = kErrorNoBufs);
+    aTxtBufferSize = GetLength();
+    VerifyOrExit(VerifyTxtData(aTxtBuffer, aTxtBufferSize, /* aAllowEmpty */ true), error = kErrorParse);
+
 exit:
     return error;
 }
diff --git a/src/core/net/dns_types.hpp b/src/core/net/dns_types.hpp
index 2fa3f1a..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.
@@ -990,6 +990,20 @@
      */
     static bool IsSubDomainOf(const char *aName, const char *aDomain);
 
+    /**
+     * This static method tests if the two DNS name are the same domain.
+     *
+     * Both @p aDomain1 and @p aDomain2 can end without dot ('.').
+     *
+     * @param[in]  aDomain1  The dot-separated name.
+     * @param[in]  aDomain2  The dot-separated domain.
+     *
+     * @retval  TRUE   If the two DNS names are the same domain.
+     * @retval  FALSE  If the two DNS names are not the same domain.
+     *
+     */
+    static bool IsSameDomain(const char *aDomain1, const char *aDomain2);
+
 private:
     // The first 2 bits of the encoded label specifies label type.
     //
@@ -1006,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
     {
@@ -1042,7 +1056,7 @@
     {
     }
 
-    const char *   mString;  // String containing the name or `nullptr` if name is not from string.
+    const char    *mString;  // String containing the name or `nullptr` if name is not from string.
     const Message *mMessage; // Message containing the encoded name, or `nullptr` if `Name` is not from message.
     uint16_t       mOffset;  // Offset in `mMessage` to the start of name (used when name is from `mMessage`).
 };
@@ -1119,7 +1133,7 @@
         uint16_t    GetTxtDataPosition(void) const { return mData[kIndexTxtPosition]; }
         void        SetTxtDataPosition(uint16_t aValue) { mData[kIndexTxtPosition] = aValue; }
         void        IncreaseTxtDataPosition(uint16_t aIncrement) { mData[kIndexTxtPosition] += aIncrement; }
-        char *      GetKeyBuffer(void) { return mChar; }
+        char       *GetKeyBuffer(void) { return mChar; }
         const char *GetTxtDataEnd(void) const { return GetTxtData() + GetTxtDataLength(); }
     };
 
@@ -1255,7 +1269,7 @@
      * @returns TRUE if the resources records matches @p aType and @p aClass, FALSE otherwise.
      *
      */
-    bool Matches(uint16_t aType, uint16_t aClass = kClassInternet)
+    bool Matches(uint16_t aType, uint16_t aClass = kClassInternet) const
     {
         return (mType == HostSwap16(aType)) && (mClass == HostSwap16(aClass));
     }
@@ -1405,11 +1419,11 @@
      */
     template <class RecordType>
     static Error FindRecord(const Message &aMessage,
-                            uint16_t &     aOffset,
+                            uint16_t      &aOffset,
                             uint16_t       aNumRecords,
                             uint16_t       aIndex,
-                            const Name &   aName,
-                            RecordType &   aRecord)
+                            const Name    &aName,
+                            RecordType    &aRecord)
     {
         return FindRecord(aMessage, aOffset, aNumRecords, aIndex, aName, RecordType::kType, aRecord,
                           sizeof(RecordType));
@@ -1456,9 +1470,9 @@
 
 protected:
     Error ReadName(const Message &aMessage,
-                   uint16_t &     aOffset,
+                   uint16_t      &aOffset,
                    uint16_t       aStartOffset,
-                   char *         aNameBuffer,
+                   char          *aNameBuffer,
                    uint16_t       aNameBufferSize,
                    bool           aSkipRecord) const;
     Error SkipRecord(const Message &aMessage, uint16_t &aOffset) const;
@@ -1466,17 +1480,17 @@
 private:
     static constexpr uint16_t kType = kTypeAny; // This is intended for used by `ReadRecord<RecordType>()` only.
 
-    static Error FindRecord(const Message & aMessage,
-                            uint16_t &      aOffset,
+    static Error FindRecord(const Message  &aMessage,
+                            uint16_t       &aOffset,
                             uint16_t        aNumRecords,
                             uint16_t        aIndex,
-                            const Name &    aName,
+                            const Name     &aName,
                             uint16_t        aType,
                             ResourceRecord &aRecord,
                             uint16_t        aMinRecordSize);
 
-    static Error ReadRecord(const Message & aMessage,
-                            uint16_t &      aOffset,
+    static Error ReadRecord(const Message  &aMessage,
+                            uint16_t       &aOffset,
                             uint16_t        aType,
                             ResourceRecord &aRecord,
                             uint16_t        aMinRecordSize);
@@ -1574,8 +1588,8 @@
      *
      */
     Error ReadCanonicalName(const Message &aMessage,
-                            uint16_t &     aOffset,
-                            char *         aNameBuffer,
+                            uint16_t      &aOffset,
+                            char          *aNameBuffer,
                             uint16_t       aNameBufferSize) const
     {
         return ResourceRecord::ReadName(aMessage, aOffset, /* aStartOffset */ aOffset - sizeof(CnameRecord),
@@ -1661,10 +1675,10 @@
      *
      */
     Error ReadPtrName(const Message &aMessage,
-                      uint16_t &     aOffset,
-                      char *         aLabelBuffer,
+                      uint16_t      &aOffset,
+                      char          *aLabelBuffer,
                       uint8_t        aLabelBufferSize,
-                      char *         aNameBuffer,
+                      char          *aNameBuffer,
                       uint16_t       aNameBufferSize) const;
 
 } OT_TOOL_PACKED_END;
@@ -1692,7 +1706,8 @@
     /**
      * This method parses and reads the TXT record data from a message.
      *
-     * This method also checks if the TXT data is well-formed by calling `VerifyTxtData()`.
+     * This method also checks if the TXT data is well-formed by calling `VerifyTxtData()` when it is successfully
+     * read.
      *
      * @param[in]      aMessage         The message to read from.
      * @param[in,out]  aOffset          On input, the offset in @p aMessage to start of TXT record data.
@@ -1705,7 +1720,9 @@
      * @retval kErrorNone           The TXT data was read successfully. @p aOffset, @p aTxtBuffer and @p aTxtBufferSize
      *                              are updated.
      * @retval kErrorParse          The TXT record in @p aMessage could not be parsed (invalid format).
-     * @retval kErrorNoBufs         TXT data could not fit in @p aTxtBufferSize bytes.
+     * @retval kErrorNoBufs         TXT data could not fit in @p aTxtBufferSize bytes. TXT data is still partially read
+     *                              into @p aTxtBuffer up to its size and @p aOffset is updated to skip over the full
+     *                              TXT record.
      *
      */
     Error ReadTxtData(const Message &aMessage, uint16_t &aOffset, uint8_t *aTxtBuffer, uint16_t &aTxtBufferSize) const;
@@ -1863,8 +1880,8 @@
      *
      */
     Error ReadTargetHostName(const Message &aMessage,
-                             uint16_t &     aOffset,
-                             char *         aNameBuffer,
+                             uint16_t      &aOffset,
+                             char          *aNameBuffer,
                              uint16_t       aNameBufferSize) const
     {
         return ResourceRecord::ReadName(aMessage, aOffset, /* aStartOffset */ aOffset - sizeof(SrvRecord), aNameBuffer,
@@ -2344,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.
@@ -2475,23 +2492,40 @@
 class LeaseOption : public Option
 {
 public:
-    static constexpr uint16_t kOptionLength = sizeof(uint32_t) + sizeof(uint32_t); ///< lease and key lease values
-
     /**
-     * This method initialize the Update Lease Option by setting the Option Code and Option Length.
+     * This method initializes the Update Lease Option using the short variant format which contains lease interval
+     * only.
      *
-     * The lease and key lease intervals remain unchanged/uninitialized.
+     * @param[in] aLeaseInterval     The lease interval in seconds.
      *
      */
-    void Init(void)
-    {
-        SetOptionCode(kUpdateLease);
-        SetOptionLength(kOptionLength);
-    }
+    void InitAsShortVariant(uint32_t aLeaseInterval);
+
+    /**
+     * This method initializes the Update Lease Option using the long variant format which contains both lease and
+     * key lease intervals.
+     *
+     * @param[in] aLeaseInterval     The lease interval in seconds.
+     * @param[in] aKeyLeaseInterval  The key lease interval in seconds.
+     *
+     */
+    void InitAsLongVariant(uint32_t aLeaseInterval, uint32_t aKeyLeaseInterval);
+
+    /**
+     * This method indicates whether or not the Update Lease Option follows the short variant format which contains
+     * only the lease interval.
+     *
+     * @retval TRUE   The Update Lease Option follows the short variant format.
+     * @retval FALSE  The Update Lease Option follows the long variant format.
+     *
+     */
+    bool IsShortVariant(void) const { return (GetOptionLength() == kShortLength); }
 
     /**
      * This method tells whether this is a valid Lease Option.
      *
+     * This method validates that option follows either short or long variant format.
+     *
      * @returns  TRUE if this is a valid Lease Option, FALSE if not a valid Lease Option.
      *
      */
@@ -2506,30 +2540,42 @@
     uint32_t GetLeaseInterval(void) const { return HostSwap32(mLeaseInterval); }
 
     /**
-     * This method sets the Update Lease OPT record's lease interval value.
-     *
-     * @param[in]  aLeaseInterval  The lease interval value.
-     *
-     */
-    void SetLeaseInterval(uint32_t aLeaseInterval) { mLeaseInterval = HostSwap32(aLeaseInterval); }
-
-    /**
      * This method returns the Update Lease OPT record's key lease interval value.
      *
+     * If the Update Lease Option follows the short variant format the lease interval is returned as key lease interval.
+     *
      * @returns The key lease interval value (in seconds).
      *
      */
-    uint32_t GetKeyLeaseInterval(void) const { return HostSwap32(mKeyLeaseInterval); }
+    uint32_t GetKeyLeaseInterval(void) const
+    {
+        return IsShortVariant() ? GetLeaseInterval() : HostSwap32(mKeyLeaseInterval);
+    }
 
     /**
-     * This method sets the Update Lease OPT record's key lease interval value.
+     * This method searches among the Options is a given message and reads and validates the Update Lease Option if
+     * found.
      *
-     * @param[in]  aKeyLeaseInterval  The key lease interval value (in seconds).
+     * This method reads the Update Lease Option whether it follows the short or long variant formats.
+     *
+     * @param[in] aMessage   The message to read the Option from.
+     * @param[in] aOffset    Offset in @p aMessage to the start of Options (start of OPT Record data).
+     * @param[in] aLength    Length of Option data in OPT record.
+     *
+     * @retval kErrorNone      Successfully read and validated the Update Lease Option from @p aMessage.
+     * @retval kErrorNotFound  Did not find any Update Lease Option.
+     * @retval kErrorParse     Failed to parse the Options.
      *
      */
-    void SetKeyLeaseInterval(uint32_t aKeyLeaseInterval) { mKeyLeaseInterval = HostSwap32(aKeyLeaseInterval); }
+    Error ReadFrom(const Message &aMessage, uint16_t aOffset, uint16_t aLength);
 
 private:
+    static constexpr uint16_t kShortLength = sizeof(uint32_t);                    // lease only.
+    static constexpr uint16_t kLongLength  = sizeof(uint32_t) + sizeof(uint32_t); // lease and key lease values
+
+    void SetLeaseInterval(uint32_t aLeaseInterval) { mLeaseInterval = HostSwap32(aLeaseInterval); }
+    void SetKeyLeaseInterval(uint32_t aKeyLeaseInterval) { mKeyLeaseInterval = HostSwap32(aKeyLeaseInterval); }
+
     uint32_t mLeaseInterval;
     uint32_t mKeyLeaseInterval;
 } OT_TOOL_PACKED_END;
diff --git a/src/core/net/dnssd_server.cpp b/src/core/net/dnssd_server.cpp
index c996c15..8f93096 100644
--- a/src/core/net/dnssd_server.cpp
+++ b/src/core/net/dnssd_server.cpp
@@ -35,6 +35,8 @@
 
 #if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
 
+#include <openthread/platform/dns.h>
+
 #include "common/array.hpp"
 #include "common/as_core_type.hpp"
 #include "common/code_utils.hpp"
@@ -52,10 +54,11 @@
 
 RegisterLogModule("DnssdServer");
 
-const char Server::kDnssdProtocolUdp[]  = "_udp";
-const char Server::kDnssdProtocolTcp[]  = "_tcp";
-const char Server::kDnssdSubTypeLabel[] = "._sub.";
-const char Server::kDefaultDomainName[] = "default.service.arpa.";
+const char  Server::kDnssdProtocolUdp[]  = "_udp";
+const char  Server::kDnssdProtocolTcp[]  = "_tcp";
+const char  Server::kDnssdSubTypeLabel[] = "._sub.";
+const char  Server::kDefaultDomainName[] = "default.service.arpa.";
+const char *Server::kBlockedDomains[]    = {"ipv4only.arpa."};
 
 Server::Server(Instance &aInstance)
     : InstanceLocator(aInstance)
@@ -63,7 +66,11 @@
     , mQueryCallbackContext(nullptr)
     , mQuerySubscribe(nullptr)
     , mQueryUnsubscribe(nullptr)
-    , mTimer(aInstance, Server::HandleTimer)
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    , mEnableUpstreamQuery(false)
+#endif
+    , mTimer(aInstance)
+    , mTestMode(kTestModeDisabled)
 {
     mCounters.Clear();
 }
@@ -75,7 +82,7 @@
     VerifyOrExit(!IsRunning());
 
     SuccessOrExit(error = mSocket.Open(&Server::HandleUdpReceive, this));
-    SuccessOrExit(error = mSocket.Bind(kPort, kBindUnspecifiedNetif ? OT_NETIF_UNSPECIFIED : OT_NETIF_THREAD));
+    SuccessOrExit(error = mSocket.Bind(kPort, kBindUnspecifiedNetif ? Ip6::kNetifUnspecified : Ip6::kNetifThread));
 
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
     Get<Srp::Server>().HandleDnssdServerStateChange();
@@ -103,6 +110,16 @@
         }
     }
 
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    for (UpstreamQueryTransaction &txn : mUpstreamQueryTransactions)
+    {
+        if (txn.IsValid())
+        {
+            ResetUpstreamQueryTransaction(txn, kErrorFailed);
+        }
+    }
+#endif
+
     mTimer.Stop();
 
     IgnoreError(mSocket.Close());
@@ -142,13 +159,27 @@
 void Server::ProcessQuery(const Header &aRequestHeader, Message &aRequestMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error            error           = kErrorNone;
-    Message *        responseMessage = nullptr;
+    Message         *responseMessage = nullptr;
     Header           responseHeader;
     NameCompressInfo compressInfo(kDefaultDomainName);
-    Header::Response response                = Header::kResponseSuccess;
-    bool             resolveByQueryCallbacks = false;
+    Header::Response response           = Header::kResponseSuccess;
+    bool             shouldSendResponse = true;
 
-    responseMessage = mSocket.NewMessage(0);
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    if (mEnableUpstreamQuery && ShouldForwardToUpstream(aRequestHeader, aRequestMessage))
+    {
+        error = ResolveByUpstream(aRequestMessage, aMessageInfo);
+        if (error == kErrorNone)
+        {
+            shouldSendResponse = false;
+            ExitNow();
+        }
+        response = Header::kResponseServerFailure;
+        LogWarn("Failed to forward DNS query to upstream: %s", ErrorToString(error));
+    }
+#endif
+
+    responseMessage = mSocket.NewMessage();
     VerifyOrExit(responseMessage != nullptr, error = kErrorNoBufs);
 
     // Allocate space for DNS header
@@ -158,6 +189,14 @@
     responseHeader.Clear();
     responseHeader.SetType(Header::kTypeResponse);
     responseHeader.SetMessageId(aRequestHeader.GetMessageId());
+    responseHeader.SetQueryType(aRequestHeader.GetQueryType());
+    if (aRequestHeader.IsRecursionDesiredFlagSet())
+    {
+        responseHeader.SetRecursionDesiredFlag();
+    }
+
+    // We may met errors when forwarding the query to the upstream
+    VerifyOrExit(response == Header::kResponseSuccess);
 
     // Validate the query
     VerifyOrExit(aRequestHeader.GetQueryType() == Header::kQueryTypeStandard,
@@ -165,6 +204,15 @@
     VerifyOrExit(!aRequestHeader.IsTruncationFlagSet(), response = Header::kResponseFormatError);
     VerifyOrExit(aRequestHeader.GetQuestionCount() > 0, response = Header::kResponseFormatError);
 
+    switch (mTestMode)
+    {
+    case kTestModeDisabled:
+        break;
+    case kTestModeSingleQuestionOnly:
+        VerifyOrExit(aRequestHeader.GetQuestionCount() == 1, response = Header::kResponseFormatError);
+        break;
+    }
+
     response = AddQuestions(aRequestHeader, aRequestMessage, responseHeader, *responseMessage, compressInfo);
     VerifyOrExit(response == Header::kResponseSuccess);
 
@@ -172,13 +220,11 @@
     // Answer the questions
     response = ResolveBySrp(responseHeader, *responseMessage, compressInfo);
 #endif
-
-    // Resolve the question using query callbacks if SRP server failed to resolve the questions.
     if (responseHeader.GetAnswerCount() == 0)
     {
         if (kErrorNone == ResolveByQueryCallbacks(responseHeader, *responseMessage, compressInfo, aMessageInfo))
         {
-            resolveByQueryCallbacks = true;
+            shouldSendResponse = false;
         }
     }
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
@@ -189,7 +235,7 @@
 #endif
 
 exit:
-    if (error == kErrorNone && !resolveByQueryCallbacks)
+    if (error == kErrorNone && shouldSendResponse)
     {
         SendResponse(responseHeader, response, *responseMessage, aMessageInfo, mSocket);
     }
@@ -199,9 +245,9 @@
 
 void Server::SendResponse(Header                  aHeader,
                           Header::Response        aResponseCode,
-                          Message &               aMessage,
+                          Message                &aMessage,
                           const Ip6::MessageInfo &aMessageInfo,
-                          Ip6::Udp::Socket &      aSocket)
+                          Ip6::Udp::Socket       &aSocket)
 {
     Error error;
 
@@ -219,10 +265,10 @@
 
     error = aSocket.SendTo(aMessage, aMessageInfo);
 
-    FreeMessageOnError(&aMessage, error);
-
     if (error != kErrorNone)
     {
+        // do not use `FreeMessageOnError()` to avoid null check on nonnull pointer
+        aMessage.Free();
         LogWarn("failed to send DNS-SD reply: %s", ErrorToString(error));
     }
     else
@@ -233,10 +279,10 @@
     UpdateResponseCounters(aResponseCode);
 }
 
-Header::Response Server::AddQuestions(const Header &    aRequestHeader,
-                                      const Message &   aRequestMessage,
-                                      Header &          aResponseHeader,
-                                      Message &         aResponseMessage,
+Header::Response Server::AddQuestions(const Header     &aRequestHeader,
+                                      const Message    &aRequestMessage,
+                                      Header           &aResponseHeader,
+                                      Message          &aResponseMessage,
                                       NameCompressInfo &aCompressInfo)
 {
     Question         question;
@@ -294,9 +340,9 @@
     return response;
 }
 
-Error Server::AppendQuestion(const char *      aName,
-                             const Question &  aQuestion,
-                             Message &         aMessage,
+Error Server::AppendQuestion(const char       *aName,
+                             const Question   &aQuestion,
+                             Message          &aMessage,
                              NameCompressInfo &aCompressInfo)
 {
     Error error = kErrorNone;
@@ -323,9 +369,9 @@
     return error;
 }
 
-Error Server::AppendPtrRecord(Message &         aMessage,
-                              const char *      aServiceName,
-                              const char *      aInstanceName,
+Error Server::AppendPtrRecord(Message          &aMessage,
+                              const char       *aServiceName,
+                              const char       *aInstanceName,
                               uint32_t          aTtl,
                               NameCompressInfo &aCompressInfo)
 {
@@ -350,9 +396,9 @@
     return error;
 }
 
-Error Server::AppendSrvRecord(Message &         aMessage,
-                              const char *      aInstanceName,
-                              const char *      aHostName,
+Error Server::AppendSrvRecord(Message          &aMessage,
+                              const char       *aInstanceName,
+                              const char       *aHostName,
                               uint32_t          aTtl,
                               uint16_t          aPriority,
                               uint16_t          aWeight,
@@ -383,11 +429,11 @@
     return error;
 }
 
-Error Server::AppendAaaaRecord(Message &           aMessage,
-                               const char *        aHostName,
+Error Server::AppendAaaaRecord(Message            &aMessage,
+                               const char         *aHostName,
                                const Ip6::Address &aAddress,
                                uint32_t            aTtl,
-                               NameCompressInfo &  aCompressInfo)
+                               NameCompressInfo   &aCompressInfo)
 {
     AaaaRecord aaaaRecord;
     Error      error;
@@ -496,9 +542,9 @@
     return error;
 }
 
-Error Server::AppendTxtRecord(Message &         aMessage,
-                              const char *      aInstanceName,
-                              const void *      aTxtData,
+Error Server::AppendTxtRecord(Message          &aMessage,
+                              const char       *aInstanceName,
+                              const void       *aTxtData,
                               uint16_t          aTxtLength,
                               uint32_t          aTtl,
                               NameCompressInfo &aCompressInfo)
@@ -640,10 +686,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--;
     }
@@ -658,8 +704,8 @@
 }
 
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
-Header::Response Server::ResolveBySrp(Header &                  aResponseHeader,
-                                      Message &                 aResponseMessage,
+Header::Response Server::ResolveBySrp(Header                   &aResponseHeader,
+                                      Message                  &aResponseMessage,
                                       Server::NameCompressInfo &aCompressInfo)
 {
     Question         question;
@@ -703,10 +749,10 @@
     return response;
 }
 
-Header::Response Server::ResolveQuestionBySrp(const char *      aName,
-                                              const Question &  aQuestion,
-                                              Header &          aResponseHeader,
-                                              Message &         aResponseMessage,
+Header::Response Server::ResolveQuestionBySrp(const char       *aName,
+                                              const Question   &aQuestion,
+                                              Header           &aResponseHeader,
+                                              Message          &aResponseMessage,
                                               NameCompressInfo &aCompressInfo,
                                               bool              aAdditional)
 {
@@ -807,16 +853,16 @@
     return host;
 }
 
-const Srp::Server::Service *Server::GetNextSrpService(const Srp::Server::Host &   aHost,
+const Srp::Server::Service *Server::GetNextSrpService(const Srp::Server::Host    &aHost,
                                                       const Srp::Server::Service *aService)
 {
     return aHost.FindNextService(aService, Srp::Server::kFlagsAnyTypeActiveService);
 }
 #endif // OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
 
-Error Server::ResolveByQueryCallbacks(Header &                aResponseHeader,
-                                      Message &               aResponseMessage,
-                                      NameCompressInfo &      aCompressInfo,
+Error Server::ResolveByQueryCallbacks(Header                 &aResponseHeader,
+                                      Message                &aResponseMessage,
+                                      NameCompressInfo       &aCompressInfo,
                                       const Ip6::MessageInfo &aMessageInfo)
 {
     QueryTransaction *query = nullptr;
@@ -839,8 +885,89 @@
     return error;
 }
 
-Server::QueryTransaction *Server::NewQuery(const Header &          aResponseHeader,
-                                           Message &               aResponseMessage,
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+bool Server::ShouldForwardToUpstream(const Header &aRequestHeader, const Message &aRequestMessage)
+{
+    bool     ret = true;
+    uint16_t readOffset;
+    char     name[Name::kMaxNameSize];
+
+    VerifyOrExit(aRequestHeader.IsRecursionDesiredFlagSet(), ret = false);
+    readOffset = sizeof(Header);
+
+    for (uint16_t i = 0; i < aRequestHeader.GetQuestionCount(); i++)
+    {
+        VerifyOrExit(kErrorNone == Name::ReadName(aRequestMessage, readOffset, name, sizeof(name)), ret = false);
+        readOffset += sizeof(Question);
+
+        VerifyOrExit(!Name::IsSubDomainOf(name, kDefaultDomainName), ret = false);
+        for (const char *blockedDomain : kBlockedDomains)
+        {
+            VerifyOrExit(!Name::IsSameDomain(name, blockedDomain), ret = false);
+        }
+    }
+
+exit:
+    return ret;
+}
+
+void Server::OnUpstreamQueryDone(UpstreamQueryTransaction &aQueryTransaction, Message *aResponseMessage)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(aQueryTransaction.IsValid(), error = kErrorInvalidArgs);
+
+    if (aResponseMessage != nullptr)
+    {
+        error = mSocket.SendTo(*aResponseMessage, aQueryTransaction.GetMessageInfo());
+    }
+    ResetUpstreamQueryTransaction(aQueryTransaction, error);
+    ResetTimer();
+
+exit:
+    FreeMessageOnError(aResponseMessage, error);
+}
+
+Server::UpstreamQueryTransaction *Server::AllocateUpstreamQueryTransaction(const Ip6::MessageInfo &aMessageInfo)
+{
+    UpstreamQueryTransaction *ret = nullptr;
+
+    for (UpstreamQueryTransaction &txn : mUpstreamQueryTransactions)
+    {
+        if (!txn.IsValid())
+        {
+            ret = &txn;
+            txn.Init(aMessageInfo);
+            break;
+        }
+    }
+
+    if (ret != nullptr)
+    {
+        LogInfo("Upstream query transaction %d initialized.", static_cast<int>(ret - mUpstreamQueryTransactions));
+        mTimer.FireAtIfEarlier(ret->GetExpireTime());
+    }
+
+    return ret;
+}
+
+Error Server::ResolveByUpstream(const Message &aRequestMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    Error                     error = kErrorNone;
+    UpstreamQueryTransaction *txn   = nullptr;
+
+    txn = AllocateUpstreamQueryTransaction(aMessageInfo);
+    VerifyOrExit(txn != nullptr, error = kErrorNoBufs);
+
+    otPlatDnsStartUpstreamQuery(&GetInstance(), txn, &aRequestMessage);
+
+exit:
+    return error;
+}
+#endif // OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+
+Server::QueryTransaction *Server::NewQuery(const Header           &aResponseHeader,
+                                           Message                &aResponseMessage,
                                            const NameCompressInfo &aCompressInfo,
                                            const Ip6::MessageInfo &aMessageInfo)
 {
@@ -866,8 +993,8 @@
     return newQuery;
 }
 
-bool Server::CanAnswerQuery(const QueryTransaction &          aQuery,
-                            const char *                      aServiceFullName,
+bool Server::CanAnswerQuery(const QueryTransaction           &aQuery,
+                            const char                       *aServiceFullName,
                             const otDnssdServiceInstanceInfo &aInstanceInfo)
 {
     char         name[Name::kMaxNameSize];
@@ -900,12 +1027,12 @@
     return (sdType == kDnsQueryResolveHost) && StringMatch(name, aHostFullName, kStringCaseInsensitiveMatch);
 }
 
-void Server::AnswerQuery(QueryTransaction &                aQuery,
-                         const char *                      aServiceFullName,
+void Server::AnswerQuery(QueryTransaction                 &aQuery,
+                         const char                       *aServiceFullName,
                          const otDnssdServiceInstanceInfo &aInstanceInfo)
 {
-    Header &          responseHeader  = aQuery.GetResponseHeader();
-    Message &         responseMessage = aQuery.GetResponseMessage();
+    Header           &responseHeader  = aQuery.GetResponseHeader();
+    Message          &responseMessage = aQuery.GetResponseMessage();
     Error             error           = kErrorNone;
     NameCompressInfo &compressInfo    = aQuery.GetNameCompressInfo();
 
@@ -960,8 +1087,8 @@
 
 void Server::AnswerQuery(QueryTransaction &aQuery, const char *aHostFullName, const otDnssdHostInfo &aHostInfo)
 {
-    Header &          responseHeader  = aQuery.GetResponseHeader();
-    Message &         responseMessage = aQuery.GetResponseMessage();
+    Header           &responseHeader  = aQuery.GetResponseHeader();
+    Message          &responseMessage = aQuery.GetResponseMessage();
     Error             error           = kErrorNone;
     NameCompressInfo &compressInfo    = aQuery.GetNameCompressInfo();
 
@@ -987,7 +1114,7 @@
 
 void Server::SetQueryCallbacks(otDnssdQuerySubscribeCallback   aSubscribe,
                                otDnssdQueryUnsubscribeCallback aUnsubscribe,
-                               void *                          aContext)
+                               void                           *aContext)
 {
     OT_ASSERT((aSubscribe == nullptr) == (aUnsubscribe == nullptr));
 
@@ -996,12 +1123,12 @@
     mQueryCallbackContext = aContext;
 }
 
-void Server::HandleDiscoveredServiceInstance(const char *                      aServiceFullName,
+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)
     {
@@ -1014,7 +1141,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)
     {
@@ -1056,7 +1183,7 @@
     return GetQueryTypeAndName(query->GetResponseHeader(), query->GetResponseMessage(), aName);
 }
 
-Server::DnsQueryType Server::GetQueryTypeAndName(const Header & aHeader,
+Server::DnsQueryType Server::GetQueryTypeAndName(const Header  &aHeader,
                                                  const Message &aMessage,
                                                  char (&aName)[Name::kMaxNameSize])
 {
@@ -1123,11 +1250,6 @@
     return found;
 }
 
-void Server::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Server>().HandleTimer();
-}
-
 void Server::HandleTimer(void)
 {
     TimeMilli now = TimerMilli::GetNow();
@@ -1148,6 +1270,21 @@
         }
     }
 
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    for (UpstreamQueryTransaction &query : mUpstreamQueryTransactions)
+    {
+        if (!query.IsValid())
+        {
+            continue;
+        }
+
+        if (query.GetExpireTime() <= now)
+        {
+            otPlatDnsCancelUpstreamQuery(&GetInstance(), &query);
+        }
+    }
+#endif
+
     ResetTimer();
 }
 
@@ -1158,24 +1295,26 @@
 
     for (QueryTransaction &query : mQueryTransactions)
     {
-        TimeMilli expire;
-
         if (!query.IsValid())
         {
             continue;
         }
 
-        expire = query.GetStartTime() + kQueryTimeout;
-        if (expire <= now)
-        {
-            nextExpire = now;
-        }
-        else if (expire < nextExpire)
-        {
-            nextExpire = expire;
-        }
+        nextExpire = Min(nextExpire, Max(now, query.GetStartTime() + kQueryTimeout));
     }
 
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    for (UpstreamQueryTransaction &query : mUpstreamQueryTransactions)
+    {
+        if (!query.IsValid())
+        {
+            continue;
+        }
+
+        nextExpire = Min(nextExpire, Max(now, query.GetExpireTime()));
+    }
+#endif
+
     if (nextExpire < now.GetDistantFuture())
     {
         mTimer.FireAt(nextExpire);
@@ -1202,11 +1341,11 @@
     aQuery.Finalize(aResponseCode, mSocket);
 }
 
-void Server::QueryTransaction::Init(const Header &          aResponseHeader,
-                                    Message &               aResponseMessage,
+void Server::QueryTransaction::Init(const Header           &aResponseHeader,
+                                    Message                &aResponseMessage,
                                     const NameCompressInfo &aCompressInfo,
                                     const Ip6::MessageInfo &aMessageInfo,
-                                    Instance &              aInstance)
+                                    Instance               &aInstance)
 {
     OT_ASSERT(mResponseMessage == nullptr);
 
@@ -1251,6 +1390,32 @@
     }
 }
 
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+void Server::UpstreamQueryTransaction::Init(const Ip6::MessageInfo &aMessageInfo)
+{
+    mMessageInfo = aMessageInfo;
+    mValid       = true;
+    mExpireTime  = TimerMilli::GetNow() + kQueryTimeout;
+}
+
+void Server::ResetUpstreamQueryTransaction(UpstreamQueryTransaction &aTxn, Error aError)
+{
+    int index = static_cast<int>(&aTxn - mUpstreamQueryTransactions);
+
+    // Avoid the warnings when info / warn logging is disabled.
+    OT_UNUSED_VARIABLE(index);
+    if (aError == kErrorNone)
+    {
+        LogInfo("Upstream query transaction %d completed.", index);
+    }
+    else
+    {
+        LogWarn("Upstream query transaction %d closed: %s.", index, ErrorToString(aError));
+    }
+    aTxn.Reset();
+}
+#endif
+
 } // namespace ServiceDiscovery
 } // namespace Dns
 } // namespace ot
diff --git a/src/core/net/dnssd_server.hpp b/src/core/net/dnssd_server.hpp
index de16c42..bebcf41 100644
--- a/src/core/net/dnssd_server.hpp
+++ b/src/core/net/dnssd_server.hpp
@@ -49,6 +49,10 @@
  *   This file includes definitions for the DNS-SD server.
  */
 
+struct otPlatDnsUpstreamQuery
+{
+};
+
 namespace ot {
 
 namespace Srp {
@@ -75,6 +79,62 @@
     {
     };
 
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    /**
+     * This class represents an upstream query transaction. The methods should only be used by
+     * `Dns::ServiceDiscovery::Server`.
+     *
+     */
+    class UpstreamQueryTransaction : public otPlatDnsUpstreamQuery
+    {
+    public:
+        /**
+         * This method returns whether the transaction is valid.
+         *
+         * @retval  TRUE  The transaction is valid.
+         * @retval  FALSE The transaction is not valid.
+         *
+         */
+        bool IsValid(void) const { return mValid; }
+
+        /**
+         * This method returns the time when the transaction expires.
+         *
+         * @returns The expire time of the transaction.
+         *
+         */
+        TimeMilli GetExpireTime(void) const { return mExpireTime; }
+
+        /**
+         * This method resets the transaction with a reason. The transaction will be invalid and can be reused for
+         * another upstream query after this call.
+         *
+         */
+        void Reset(void) { mValid = false; }
+
+        /**
+         * This method initializes the transaction.
+         *
+         * @param[in] aMessageInfo  The IP message info of the query.
+         *
+         */
+        void Init(const Ip6::MessageInfo &aMessageInfo);
+
+        /**
+         * This method returns the message info of the query.
+         *
+         * @returns  The message info of the query.
+         *
+         */
+        const Ip6::MessageInfo &GetMessageInfo(void) const { return mMessageInfo; }
+
+    private:
+        Ip6::MessageInfo mMessageInfo;
+        TimeMilli        mExpireTime;
+        bool             mValid;
+    };
+#endif
+
     /**
      * This enumeration specifies a DNS-SD query type.
      *
@@ -122,7 +182,7 @@
      */
     void SetQueryCallbacks(otDnssdQuerySubscribeCallback   aSubscribe,
                            otDnssdQueryUnsubscribeCallback aUnsubscribe,
-                           void *                          aContext);
+                           void                           *aContext);
 
     /**
      * This method notifies a discovered service instance.
@@ -133,6 +193,37 @@
      */
     void HandleDiscoveredServiceInstance(const char *aServiceFullName, const otDnssdServiceInstanceInfo &aInstanceInfo);
 
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    /**
+     * This method notifies an answer of an upstream DNS query.
+     *
+     * The Transaction will be released.
+     *
+     * @param[in] aQueryTransaction    A reference to upstream DNS query transaction.
+     * @param[in] aResponseMessage     A pointer to response UDP message, should be allocated from Udp::NewMessage.
+     *                                 Passing a nullptr means close the transaction without a response.
+     *
+     */
+    void OnUpstreamQueryDone(UpstreamQueryTransaction &aQueryTransaction, Message *aResponseMessage);
+
+    /**
+     * This method indicates whether the server will forward DNS queries to platform DNS upstream API.
+     *
+     * @retval TRUE  If the server will forward DNS queries.
+     * @retval FALSE If the server will not forward DNS queries.
+     *
+     */
+    bool IsUpstreamQueryEnabled(void) const { return mEnableUpstreamQuery; }
+
+    /**
+     * This method enables or disables forwarding DNS queries to platform DNS upstream API.
+     *
+     * @param[in]  aEnabled   A boolean to enable/disable forwarding DNS queries to upstream.
+     *
+     */
+    void SetUpstreamQueryEnabled(bool aEnabled) { mEnableUpstreamQuery = aEnabled; }
+#endif // OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+
     /**
      * This method notifies a discovered host.
      *
@@ -171,6 +262,27 @@
      */
     const Counters &GetCounters(void) const { return mCounters; };
 
+    /**
+     * This enumeration represents different test modes.
+     *
+     * The test mode is 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).
+     *
+     */
+    enum TestMode : uint8_t
+    {
+        kTestModeDisabled,           ///< Test mode is disabled.
+        kTestModeSingleQuestionOnly, ///< Allow single question in query message, send `FormatError` for two or more.
+    };
+
+    /**
+     * This method sets the test mode for `Server`.
+     *
+     * @param[in] aTestMode   The new test mode.
+     *
+     */
+    void SetTestMode(TestMode aTestMode) { mTestMode = aTestMode; }
+
 private:
     class NameCompressInfo : public Clearable<NameCompressInfo>
     {
@@ -250,10 +362,11 @@
         uint16_t    mHostNameOffset;     // Offset of host name serialization into the response message.
     };
 
-    static constexpr bool     kBindUnspecifiedNetif = OPENTHREAD_CONFIG_DNSSD_SERVER_BIND_UNSPECIFIED_NETIF;
-    static constexpr uint8_t  kProtocolLabelLength  = 4;
-    static constexpr uint8_t  kSubTypeLabelLength   = 4;
-    static constexpr uint16_t kMaxConcurrentQueries = 32;
+    static constexpr bool     kBindUnspecifiedNetif         = OPENTHREAD_CONFIG_DNSSD_SERVER_BIND_UNSPECIFIED_NETIF;
+    static constexpr uint8_t  kProtocolLabelLength          = 4;
+    static constexpr uint8_t  kSubTypeLabelLength           = 4;
+    static constexpr uint16_t kMaxConcurrentQueries         = 32;
+    static constexpr uint16_t kMaxConcurrentUpstreamQueries = 32;
 
     // This structure represents the splitting information of a full name.
     struct NameComponentsOffsetInfo
@@ -297,23 +410,23 @@
         {
         }
 
-        void                    Init(const Header &          aResponseHeader,
-                                     Message &               aResponseMessage,
+        void                    Init(const Header           &aResponseHeader,
+                                     Message                &aResponseMessage,
                                      const NameCompressInfo &aCompressInfo,
                                      const Ip6::MessageInfo &aMessageInfo,
-                                     Instance &              aInstance);
+                                     Instance               &aInstance);
         bool                    IsValid(void) const { return mResponseMessage != nullptr; }
         const Ip6::MessageInfo &GetMessageInfo(void) const { return mMessageInfo; }
-        const Header &          GetResponseHeader(void) const { return mResponseHeader; }
-        Header &                GetResponseHeader(void) { return mResponseHeader; }
-        const Message &         GetResponseMessage(void) const { return *mResponseMessage; }
-        Message &               GetResponseMessage(void) { return *mResponseMessage; }
+        const Header           &GetResponseHeader(void) const { return mResponseHeader; }
+        Header                 &GetResponseHeader(void) { return mResponseHeader; }
+        const Message          &GetResponseMessage(void) const { return *mResponseMessage; }
+        Message                &GetResponseMessage(void) { return const_cast<Message &>(*mResponseMessage); }
         TimeMilli               GetStartTime(void) const { return mStartTime; }
-        NameCompressInfo &      GetNameCompressInfo(void) { return mCompressInfo; };
+        NameCompressInfo       &GetNameCompressInfo(void) { return mCompressInfo; };
         void                    Finalize(Header::Response aResponseMessage, Ip6::Udp::Socket &aSocket);
 
         Header           mResponseHeader;
-        Message *        mResponseMessage;
+        Message         *mResponseMessage;
         NameCompressInfo mCompressInfo;
         Ip6::MessageInfo mMessageInfo;
         TimeMilli        mStartTime;
@@ -325,39 +438,39 @@
     static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
     void        HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
     void ProcessQuery(const Header &aRequestHeader, Message &aRequestMessage, const Ip6::MessageInfo &aMessageInfo);
-    static Header::Response AddQuestions(const Header &    aRequestHeader,
-                                         const Message &   aRequestMessage,
-                                         Header &          aResponseHeader,
-                                         Message &         aResponseMessage,
+    static Header::Response AddQuestions(const Header     &aRequestHeader,
+                                         const Message    &aRequestMessage,
+                                         Header           &aResponseHeader,
+                                         Message          &aResponseMessage,
                                          NameCompressInfo &aCompressInfo);
-    static Error            AppendQuestion(const char *      aName,
-                                           const Question &  aQuestion,
-                                           Message &         aMessage,
+    static Error            AppendQuestion(const char       *aName,
+                                           const Question   &aQuestion,
+                                           Message          &aMessage,
                                            NameCompressInfo &aCompressInfo);
-    static Error            AppendPtrRecord(Message &         aMessage,
-                                            const char *      aServiceName,
-                                            const char *      aInstanceName,
+    static Error            AppendPtrRecord(Message          &aMessage,
+                                            const char       *aServiceName,
+                                            const char       *aInstanceName,
                                             uint32_t          aTtl,
                                             NameCompressInfo &aCompressInfo);
-    static Error            AppendSrvRecord(Message &         aMessage,
-                                            const char *      aInstanceName,
-                                            const char *      aHostName,
+    static Error            AppendSrvRecord(Message          &aMessage,
+                                            const char       *aInstanceName,
+                                            const char       *aHostName,
                                             uint32_t          aTtl,
                                             uint16_t          aPriority,
                                             uint16_t          aWeight,
                                             uint16_t          aPort,
                                             NameCompressInfo &aCompressInfo);
-    static Error            AppendTxtRecord(Message &         aMessage,
-                                            const char *      aInstanceName,
-                                            const void *      aTxtData,
+    static Error            AppendTxtRecord(Message          &aMessage,
+                                            const char       *aInstanceName,
+                                            const void       *aTxtData,
                                             uint16_t          aTxtLength,
                                             uint32_t          aTtl,
                                             NameCompressInfo &aCompressInfo);
-    static Error            AppendAaaaRecord(Message &           aMessage,
-                                             const char *        aHostName,
+    static Error            AppendAaaaRecord(Message            &aMessage,
+                                             const char         *aHostName,
                                              const Ip6::Address &aAddress,
                                              uint32_t            aTtl,
-                                             NameCompressInfo &  aCompressInfo);
+                                             NameCompressInfo   &aCompressInfo);
     static Error            AppendServiceName(Message &aMessage, const char *aName, NameCompressInfo &aCompressInfo);
     static Error            AppendInstanceName(Message &aMessage, const char *aName, NameCompressInfo &aCompressInfo);
     static Error            AppendHostName(Message &aMessage, const char *aName, NameCompressInfo &aCompressInfo);
@@ -366,70 +479,90 @@
     static Error            FindPreviousLabel(const char *aName, uint8_t &aStart, uint8_t &aStop);
     void                    SendResponse(Header                  aHeader,
                                          Header::Response        aResponseCode,
-                                         Message &               aMessage,
+                                         Message                &aMessage,
                                          const Ip6::MessageInfo &aMessageInfo,
-                                         Ip6::Udp::Socket &      aSocket);
+                                         Ip6::Udp::Socket       &aSocket);
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
-    Header::Response                   ResolveBySrp(Header &                  aResponseHeader,
-                                                    Message &                 aResponseMessage,
+    Header::Response                   ResolveBySrp(Header                   &aResponseHeader,
+                                                    Message                  &aResponseMessage,
                                                     Server::NameCompressInfo &aCompressInfo);
-    Header::Response                   ResolveQuestionBySrp(const char *      aName,
-                                                            const Question &  aQuestion,
-                                                            Header &          aResponseHeader,
-                                                            Message &         aResponseMessage,
+    Header::Response                   ResolveQuestionBySrp(const char       *aName,
+                                                            const Question   &aQuestion,
+                                                            Header           &aResponseHeader,
+                                                            Message          &aResponseMessage,
                                                             NameCompressInfo &aCompressInfo,
                                                             bool              aAdditional);
-    const Srp::Server::Host *          GetNextSrpHost(const Srp::Server::Host *aHost);
-    static const Srp::Server::Service *GetNextSrpService(const Srp::Server::Host &   aHost,
+    const Srp::Server::Host           *GetNextSrpHost(const Srp::Server::Host *aHost);
+    static const Srp::Server::Service *GetNextSrpService(const Srp::Server::Host    &aHost,
                                                          const Srp::Server::Service *aService);
 #endif
 
-    Error             ResolveByQueryCallbacks(Header &                aResponseHeader,
-                                              Message &               aResponseMessage,
-                                              NameCompressInfo &      aCompressInfo,
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    static bool               ShouldForwardToUpstream(const Header &aRequestHeader, const Message &aRequestMessage);
+    UpstreamQueryTransaction *AllocateUpstreamQueryTransaction(const Ip6::MessageInfo &aMessageInfo);
+    void                      ResetUpstreamQueryTransaction(UpstreamQueryTransaction &aTxn, Error aError);
+    Error                     ResolveByUpstream(const Message &aRequestMessage, const Ip6::MessageInfo &aMessageInfo);
+#endif
+
+    Error             ResolveByQueryCallbacks(Header                 &aResponseHeader,
+                                              Message                &aResponseMessage,
+                                              NameCompressInfo       &aCompressInfo,
                                               const Ip6::MessageInfo &aMessageInfo);
-    QueryTransaction *NewQuery(const Header &          aResponseHeader,
-                               Message &               aResponseMessage,
+    QueryTransaction *NewQuery(const Header           &aResponseHeader,
+                               Message                &aResponseMessage,
                                const NameCompressInfo &aCompressInfo,
                                const Ip6::MessageInfo &aMessageInfo);
-    static bool       CanAnswerQuery(const QueryTransaction &          aQuery,
-                                     const char *                      aServiceFullName,
+    static bool       CanAnswerQuery(const QueryTransaction           &aQuery,
+                                     const char                       *aServiceFullName,
                                      const otDnssdServiceInstanceInfo &aInstanceInfo);
-    void              AnswerQuery(QueryTransaction &                aQuery,
-                                  const char *                      aServiceFullName,
+    void              AnswerQuery(QueryTransaction                 &aQuery,
+                                  const char                       *aServiceFullName,
                                   const otDnssdServiceInstanceInfo &aInstanceInfo);
     static bool       CanAnswerQuery(const Server::QueryTransaction &aQuery, const char *aHostFullName);
     void AnswerQuery(QueryTransaction &aQuery, const char *aHostFullName, const otDnssdHostInfo &aHostInfo);
     void FinalizeQuery(QueryTransaction &aQuery, Header::Response aResponseCode);
-    static DnsQueryType GetQueryTypeAndName(const Header & aHeader,
+    static DnsQueryType GetQueryTypeAndName(const Header  &aHeader,
                                             const Message &aMessage,
                                             char (&aName)[Name::kMaxNameSize]);
     static bool HasQuestion(const Header &aHeader, const Message &aMessage, const char *aName, uint16_t aQuestionType);
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
-    void        ResetTimer(void);
+
+    void HandleTimer(void);
+    void ResetTimer(void);
 
     void UpdateResponseCounters(Header::Response aResponseCode);
 
+    using ServerTimer = TimerMilliIn<Server, &Server::HandleTimer>;
+
     static const char kDnssdProtocolUdp[];
     static const char kDnssdProtocolTcp[];
     static const char kDnssdSubTypeLabel[];
     static const char kDefaultDomainName[];
-    Ip6::Udp::Socket  mSocket;
+
+    Ip6::Udp::Socket mSocket;
 
     QueryTransaction                mQueryTransactions[kMaxConcurrentQueries];
-    void *                          mQueryCallbackContext;
+    void                           *mQueryCallbackContext;
     otDnssdQuerySubscribeCallback   mQuerySubscribe;
     otDnssdQueryUnsubscribeCallback mQueryUnsubscribe;
-    TimerMilli                      mTimer;
 
-    Counters mCounters;
+    static const char *kBlockedDomains[];
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    bool                     mEnableUpstreamQuery;
+    UpstreamQueryTransaction mUpstreamQueryTransactions[kMaxConcurrentUpstreamQueries];
+#endif
+
+    ServerTimer mTimer;
+    Counters    mCounters;
+    TestMode    mTestMode;
 };
 
 } // namespace ServiceDiscovery
 } // namespace Dns
 
 DefineMapEnum(otDnssdQueryType, Dns::ServiceDiscovery::Server::DnsQueryType);
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+DefineCoreType(otPlatDnsUpstreamQuery, Dns::ServiceDiscovery::Server::UpstreamQueryTransaction);
+#endif
 
 } // namespace ot
 
diff --git a/src/core/net/icmp6.cpp b/src/core/net/icmp6.cpp
index 93542f6..2f195b0 100644
--- a/src/core/net/icmp6.cpp
+++ b/src/core/net/icmp6.cpp
@@ -54,15 +54,9 @@
 {
 }
 
-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);
-}
+Error Icmp::RegisterHandler(Handler &aHandler) { return mHandlers.Add(aHandler); }
 
 Error Icmp::SendEchoRequest(Message &aMessage, const MessageInfo &aMessageInfo, uint16_t aIdentifier)
 {
@@ -103,7 +97,7 @@
 {
     Error             error = kErrorNone;
     MessageInfo       messageInfoLocal;
-    Message *         message = nullptr;
+    Message          *message = nullptr;
     Header            icmp6Header;
     Message::Settings settings(Message::kWithLinkSecurity, Message::kPriorityNet);
 
@@ -186,9 +180,9 @@
 {
     Error       error = kErrorNone;
     Header      icmp6Header;
-    Message *   replyMessage = nullptr;
+    Message    *replyMessage = nullptr;
     MessageInfo replyMessageInfo;
-    uint16_t    payloadLength;
+    uint16_t    dataOffset;
 
     // always handle Echo Request destined for RLOC or ALOC
     VerifyOrExit(ShouldHandleEchoRequest(aMessageInfo) || aMessageInfo.GetSockAddr().GetIid().IsLocator());
@@ -204,12 +198,11 @@
         ExitNow();
     }
 
-    payloadLength = aRequestMessage.GetLength() - aRequestMessage.GetOffset() - Header::kDataFieldOffset;
-    SuccessOrExit(error = replyMessage->SetLength(Header::kDataFieldOffset + payloadLength));
+    dataOffset = aRequestMessage.GetOffset() + Header::kDataFieldOffset;
 
-    replyMessage->WriteBytes(0, &icmp6Header, Header::kDataFieldOffset);
-    aRequestMessage.CopyTo(aRequestMessage.GetOffset() + Header::kDataFieldOffset, Header::kDataFieldOffset,
-                           payloadLength, *replyMessage);
+    SuccessOrExit(error = replyMessage->AppendBytes(&icmp6Header, Header::kDataFieldOffset));
+    SuccessOrExit(error = replyMessage->AppendBytesFromMessage(aRequestMessage, dataOffset,
+                                                               aRequestMessage.GetLength() - dataOffset));
 
     replyMessageInfo.SetPeerAddr(aMessageInfo.GetPeerAddr());
 
diff --git a/src/core/net/icmp6.hpp b/src/core/net/icmp6.hpp
index 594da57..b054b56 100644
--- a/src/core/net/icmp6.hpp
+++ b/src/core/net/icmp6.hpp
@@ -92,6 +92,8 @@
             kTypeEchoReply        = OT_ICMP6_TYPE_ECHO_REPLY,        ///< Echo Reply
             kTypeRouterSolicit    = OT_ICMP6_TYPE_ROUTER_SOLICIT,    ///< Router Solicitation
             kTypeRouterAdvert     = OT_ICMP6_TYPE_ROUTER_ADVERT,     ///< Router Advertisement
+            kTypeNeighborSolicit  = OT_ICMP6_TYPE_NEIGHBOR_SOLICIT,  ///< Neighbor Solicitation
+            kTypeNeighborAdvert   = OT_ICMP6_TYPE_NEIGHBOR_ADVERT,   ///< Neighbor Advertisement
         };
 
         /**
@@ -240,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 a23f51e..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:
@@ -85,7 +68,7 @@
 {
     // The prefix length must be 32, 40, 48, 56, 64, 96. IPv4 bytes are added
     // after the prefix, skipping over the bits 64 to 71 (byte at `kSkipIndex`)
-    // which must be set to zero. The suffix is set to zero (per RFC 6502).
+    // which must be set to zero. The suffix is set to zero (per RFC 6052).
     //
     //    +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     //    |PL| 0-------------32--40--48--56--64--72--80--88--96--104---------|
@@ -111,14 +94,14 @@
 
     ip6Index = aPrefixLength / CHAR_BIT;
 
-    for (uint8_t i = 0; i < Ip4::Address::kSize; i++)
+    for (uint8_t &i : mFields.m8)
     {
         if (ip6Index == kSkipIndex)
         {
             ip6Index++;
         }
 
-        mFields.m8[i] = aIp6Address.GetBytes()[ip6Index++];
+        i = aIp6Address.GetBytes()[ip6Index++];
     }
 }
 
@@ -127,20 +110,67 @@
     mFields.m32 = (aCidr.mAddress.mFields.m32 & aCidr.SubnetMask()) | (HostSwap32(aHost) & aCidr.HostMask());
 }
 
+void Address::ToString(StringWriter &aWriter) const
+{
+    aWriter.Append("%d.%d.%d.%d", mFields.m8[0], mFields.m8[1], mFields.m8[2], mFields.m8[3]);
+}
+
+void Address::ToString(char *aBuffer, uint16_t aSize) const
+{
+    StringWriter writer(aBuffer, aSize);
+
+    ToString(writer);
+}
+
 Address::InfoString Address::ToString(void) const
 {
     InfoString string;
 
-    string.Append("%d.%d.%d.%d", mFields.m8[0], mFields.m8[1], mFields.m8[2], mFields.m8[3]);
+    ToString(string);
 
     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);
+}
+
+void Cidr::ToString(char *aBuffer, uint16_t aSize) const
+{
+    StringWriter writer(aBuffer, aSize);
+
+    ToString(writer);
+}
+
 Cidr::InfoString Cidr::ToString(void) const
 {
     InfoString string;
 
-    string.Append("%s/%d", AsCoreType(&mAddress).ToString().AsCString(), mLength);
+    ToString(string);
 
     return string;
 }
diff --git a/src/core/net/ip4_types.hpp b/src/core/net/ip4_types.hpp
index 042e4ca..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,21 @@
      * @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.
+     *
+     * The string format uses quad-dotted notation of four bytes in the address (e.g., "127.0.0.1").
+     *
+     * 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.
+     *
+     * @param[out] aBuffer   A pointer to a char array to output the string (MUST NOT be `nullptr`).
+     * @param[in]  aSize     The size of @p aBuffer (in bytes).
+     *
+     */
+    void ToString(char *aBuffer, uint16_t aSize) const;
 
     /**
      * This method converts the IPv4 address to a string.
@@ -170,6 +184,9 @@
      *
      */
     InfoString ToString(void) const;
+
+private:
+    void ToString(StringWriter &aWriter) const;
 } OT_TOOL_PACKED_END;
 
 /**
@@ -190,6 +207,35 @@
     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.,
+     * "127.0.0.1/32").
+     *
+     * 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.
+     *
+     * @param[out] aBuffer   A pointer to a char array to output the string (MUST NOT be `nullptr`).
+     * @param[in]  aSize     The size of @p aBuffer (in bytes).
+     *
+     */
+    void ToString(char *aBuffer, uint16_t aSize) const;
+
+    /**
      * 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.,
@@ -237,6 +283,8 @@
     }
 
     uint32_t SubnetMask(void) const { return ~HostMask(); }
+
+    void ToString(StringWriter &aWriter) const;
 };
 
 /**
@@ -250,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;
@@ -479,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.
@@ -487,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.
@@ -495,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
@@ -527,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;
@@ -537,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
@@ -636,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 bb3096f..b6693d2 100644
--- a/src/core/net/ip6.cpp
+++ b/src/core/net/ip6.cpp
@@ -47,6 +47,7 @@
 #include "net/icmp6.hpp"
 #include "net/ip6_address.hpp"
 #include "net/ip6_filter.hpp"
+#include "net/nat64_translator.hpp"
 #include "net/netif.hpp"
 #include "net/udp6.hpp"
 #include "openthread/ip6.h"
@@ -66,11 +67,8 @@
 
 Ip6::Ip6(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mForwardingEnabled(false)
     , mIsReceiveIp6FilterEnabled(false)
-    , mReceiveIp6DatagramCallback(nullptr)
-    , mReceiveIp6DatagramCallbackContext(nullptr)
-    , mSendQueueTask(aInstance, Ip6::HandleSendQueue)
+    , mSendQueueTask(aInstance)
     , mIcmp(aInstance)
     , mUdp(aInstance)
     , mMpl(aInstance)
@@ -78,17 +76,36 @@
     , mTcp(aInstance)
 #endif
 {
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+    ResetBorderRoutingCounters();
+#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(OptionMpl) + aReserved, aSettings);
+        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);
 
@@ -102,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;
@@ -169,48 +174,24 @@
     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;
-}
-
-void Ip6::SetReceiveDatagramCallback(otIp6ReceiveCallback aCallback, void *aCallbackContext)
-{
-    mReceiveIp6DatagramCallback        = aCallback;
-    mReceiveIp6DatagramCallbackContext = aCallbackContext;
-}
-
 Error Ip6::AddMplOption(Message &aMessage, Header &aHeader)
 {
     Error          error = kErrorNone;
     HopByHopHeader hbhHeader;
-    OptionMpl      mplOption;
-    OptionPadN     padOption;
+    MplOption      mplOption;
+    PadOption      padOption;
 
     hbhHeader.SetNextHeader(aHeader.GetNextHeader());
     hbhHeader.SetLength(0);
     mMpl.InitOption(mplOption, aHeader.GetSource());
 
-    // Mpl option may require two bytes padding.
-    if ((mplOption.GetTotalLength() + sizeof(hbhHeader)) % 8)
+    // Check if MPL option may require padding
+    if (padOption.InitToPadHeaderWithSize(sizeof(HopByHopHeader) + mplOption.GetSize()) == kErrorNone)
     {
-        padOption.Init(2);
-        SuccessOrExit(error = aMessage.PrependBytes(&padOption, padOption.GetTotalLength()));
+        SuccessOrExit(error = aMessage.PrependBytes(&padOption, padOption.GetSize()));
     }
 
-    SuccessOrExit(error = aMessage.PrependBytes(&mplOption, mplOption.GetTotalLength()));
+    SuccessOrExit(error = aMessage.PrependBytes(&mplOption, mplOption.GetSize()));
     SuccessOrExit(error = aMessage.Prepend(hbhHeader));
     aHeader.SetPayloadLength(aHeader.GetPayloadLength() + sizeof(hbhHeader) + sizeof(mplOption));
     aHeader.SetNextHeader(kProtoHopOpts);
@@ -219,25 +200,23 @@
     return error;
 }
 
-Error Ip6::AddTunneledMplOption(Message &aMessage, Header &aHeader, MessageInfo &aMessageInfo)
+Error Ip6::AddTunneledMplOption(Message &aMessage, Header &aHeader)
 {
-    Error                        error = kErrorNone;
-    Header                       tunnelHeader;
-    const Netif::UnicastAddress *source;
-    MessageInfo                  messageInfo(aMessageInfo);
+    Error          error = kErrorNone;
+    Header         tunnelHeader;
+    const Address *source;
 
     // Use IP-in-IP encapsulation (RFC2473) and ALL_MPL_FORWARDERS address.
-    messageInfo.GetPeerAddr().SetToRealmLocalAllMplForwarders();
-
     tunnelHeader.InitVersionTrafficClassFlow();
     tunnelHeader.SetHopLimit(static_cast<uint8_t>(kDefaultHopLimit));
     tunnelHeader.SetPayloadLength(aHeader.GetPayloadLength() + sizeof(tunnelHeader));
-    tunnelHeader.SetDestination(messageInfo.GetPeerAddr());
+    tunnelHeader.GetDestination().SetToRealmLocalAllMplForwarders();
     tunnelHeader.SetNextHeader(kProtoIp6);
 
-    VerifyOrExit((source = SelectSourceAddress(messageInfo)) != nullptr, error = kErrorInvalidSourceAddress);
+    source = SelectSourceAddress(tunnelHeader.GetDestination());
+    VerifyOrExit(source != nullptr, error = kErrorInvalidSourceAddress);
 
-    tunnelHeader.SetSource(source->GetAddress());
+    tunnelHeader.SetSource(*source);
 
     SuccessOrExit(error = AddMplOption(aMessage, tunnelHeader));
     SuccessOrExit(error = aMessage.Prepend(tunnelHeader));
@@ -246,7 +225,7 @@
     return error;
 }
 
-Error Ip6::InsertMplOption(Message &aMessage, Header &aHeader, MessageInfo &aMessageInfo)
+Error Ip6::InsertMplOption(Message &aMessage, Header &aHeader)
 {
     Error error = kErrorNone;
 
@@ -260,37 +239,37 @@
         if (aHeader.GetNextHeader() == kProtoHopOpts)
         {
             HopByHopHeader hbh;
-            uint16_t       hbhLength = 0;
-            OptionMpl      mplOption;
+            uint16_t       hbhSize;
+            MplOption      mplOption;
+            PadOption      padOption;
 
-            // read existing hop-by-hop option header
+            // Read existing hop-by-hop option header
             SuccessOrExit(error = aMessage.Read(0, hbh));
-            hbhLength = (hbh.GetLength() + 1) * 8;
+            hbhSize = hbh.GetSize();
 
-            VerifyOrExit(hbhLength <= aHeader.GetPayloadLength(), error = kErrorParse);
+            VerifyOrExit(hbhSize <= aHeader.GetPayloadLength(), error = kErrorParse);
 
-            // increase existing hop-by-hop option header length by 8 bytes
+            // Increment hop-by-hop option header length by one which
+            // increases its total size by 8 bytes.
             hbh.SetLength(hbh.GetLength() + 1);
             aMessage.Write(0, hbh);
 
-            // make space for MPL Option + padding by shifting hop-by-hop option header
-            SuccessOrExit(error = aMessage.PrependBytes(nullptr, 8));
-            aMessage.CopyTo(8, 0, hbhLength, aMessage);
+            // Make space for MPL Option + padding (8 bytes) at the end
+            // of hop-by-hop header
+            SuccessOrExit(error = aMessage.InsertHeader(hbhSize, ExtensionHeader::kLengthUnitSize));
 
-            // insert MPL Option
+            // Insert MPL Option
             mMpl.InitOption(mplOption, aHeader.GetSource());
-            aMessage.WriteBytes(hbhLength, &mplOption, mplOption.GetTotalLength());
+            aMessage.WriteBytes(hbhSize, &mplOption, mplOption.GetSize());
 
-            // insert Pad Option (if needed)
-            if (mplOption.GetTotalLength() % 8)
+            // Insert Pad Option (if needed)
+            if (padOption.InitToPadHeaderWithSize(mplOption.GetSize()) == kErrorNone)
             {
-                OptionPadN padOption;
-                padOption.Init(8 - (mplOption.GetTotalLength() % 8));
-                aMessage.WriteBytes(hbhLength + mplOption.GetTotalLength(), &padOption, padOption.GetTotalLength());
+                aMessage.WriteBytes(hbhSize + mplOption.GetSize(), &padOption, padOption.GetSize());
             }
 
-            // increase IPv6 Payload Length
-            aHeader.SetPayloadLength(aHeader.GetPayloadLength() + 8);
+            // Update IPv6 Payload Length
+            aHeader.SetPayloadLength(aHeader.GetPayloadLength() + ExtensionHeader::kLengthUnitSize);
         }
         else
         {
@@ -309,7 +288,7 @@
 
             if ((messageCopy = aMessage.Clone()) != nullptr)
             {
-                IgnoreError(HandleDatagram(*messageCopy, nullptr, nullptr, /* aFromHost */ true));
+                IgnoreError(HandleDatagram(*messageCopy, kFromHostDisallowLoopBack));
                 LogInfo("Message copy for indirect transmission to sleepy children");
             }
             else
@@ -319,7 +298,7 @@
         }
 #endif
 
-        SuccessOrExit(error = AddTunneledMplOption(aMessage, aHeader, aMessageInfo));
+        SuccessOrExit(error = AddTunneledMplOption(aMessage, aHeader));
     }
 
 exit:
@@ -331,6 +310,7 @@
     Error          error = kErrorNone;
     Header         ip6Header;
     HopByHopHeader hbh;
+    Option         option;
     uint16_t       offset;
     uint16_t       endOffset;
     uint16_t       mplOffset = 0;
@@ -343,55 +323,46 @@
     VerifyOrExit(ip6Header.GetNextHeader() == kProtoHopOpts);
 
     IgnoreError(aMessage.Read(offset, hbh));
-    endOffset = offset + (hbh.GetLength() + 1) * 8;
+    endOffset = offset + hbh.GetSize();
     VerifyOrExit(aMessage.GetLength() >= endOffset, error = kErrorParse);
 
     offset += sizeof(hbh);
 
-    while (offset < endOffset)
+    for (; offset < endOffset; offset += option.GetSize())
     {
-        OptionHeader option;
+        IgnoreError(option.ParseFrom(aMessage, offset, endOffset));
 
-        IgnoreError(aMessage.Read(offset, option));
-
-        switch (option.GetType())
+        if (option.IsPadding())
         {
-        case OptionMpl::kType:
-            // if multiple MPL options exist, discard packet
+            continue;
+        }
+
+        if (option.GetType() == MplOption::kType)
+        {
+            // If multiple MPL options exist, discard packet
             VerifyOrExit(mplOffset == 0, error = kErrorParse);
 
             mplOffset = offset;
             mplLength = option.GetLength();
 
-            VerifyOrExit(mplLength <= sizeof(OptionMpl) - sizeof(OptionHeader), error = kErrorParse);
+            VerifyOrExit(mplLength <= sizeof(MplOption) - sizeof(Option), error = kErrorParse);
 
             if (mplOffset == sizeof(ip6Header) + sizeof(hbh) && hbh.GetLength() == 0)
             {
-                // first and only IPv6 Option, remove IPv6 HBH Option header
+                // First and only IPv6 Option, remove IPv6 HBH Option header
                 remove = true;
             }
-            else if (mplOffset + 8 == endOffset)
+            else if (mplOffset + ExtensionHeader::kLengthUnitSize == endOffset)
             {
-                // last IPv6 Option, remove last 8 bytes
+                // Last IPv6 Option, remove the last 8 bytes
                 remove = true;
             }
-
-            offset += sizeof(option) + option.GetLength();
-            break;
-
-        case OptionPad1::kType:
-            offset += sizeof(OptionPad1);
-            break;
-
-        case OptionPadN::kType:
-            offset += sizeof(option) + option.GetLength();
-            break;
-
-        default:
-            // encountered another option, now just replace MPL Option with PadN
+        }
+        else
+        {
+            // Encountered another option, now just replace
+            // MPL Option with Pad Option
             remove = false;
-            offset += sizeof(option) + option.GetLength();
-            break;
         }
     }
 
@@ -400,42 +371,34 @@
 
     if (remove)
     {
-        // last IPv6 Option, shrink HBH Option header
-        uint8_t buf[8];
-
-        offset = endOffset - sizeof(buf);
-
-        while (offset >= sizeof(buf))
-        {
-            IgnoreError(aMessage.Read(offset - sizeof(buf), buf));
-            aMessage.Write(offset, buf);
-            offset -= sizeof(buf);
-        }
-
-        aMessage.RemoveHeader(sizeof(buf));
+        // Last IPv6 Option, shrink HBH Option header by
+        // 8 bytes (`kLengthUnitSize`)
+        aMessage.RemoveHeader(endOffset - ExtensionHeader::kLengthUnitSize, ExtensionHeader::kLengthUnitSize);
 
         if (mplOffset == sizeof(ip6Header) + sizeof(hbh))
         {
-            // remove entire HBH header
+            // Remove entire HBH header
             ip6Header.SetNextHeader(hbh.GetNextHeader());
         }
         else
         {
-            // update HBH header length
+            // Update HBH header length, decrement by one
+            // which decreases its total size by 8 bytes.
+
             hbh.SetLength(hbh.GetLength() - 1);
             aMessage.Write(sizeof(ip6Header), hbh);
         }
 
-        ip6Header.SetPayloadLength(ip6Header.GetPayloadLength() - sizeof(buf));
+        ip6Header.SetPayloadLength(ip6Header.GetPayloadLength() - ExtensionHeader::kLengthUnitSize);
         aMessage.Write(0, ip6Header);
     }
     else if (mplOffset != 0)
     {
-        // replace MPL Option with PadN Option
-        OptionPadN padOption;
+        // Replace MPL Option with Pad Option
+        PadOption padOption;
 
-        padOption.Init(sizeof(OptionHeader) + mplLength);
-        aMessage.WriteBytes(mplOffset, &padOption, padOption.GetTotalLength());
+        padOption.InitForPadSize(sizeof(Option) + mplLength);
+        aMessage.WriteBytes(mplOffset, &padOption, padOption.GetSize());
     }
 
 exit:
@@ -452,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);
@@ -471,10 +446,10 @@
 
     if (aMessageInfo.GetSockAddr().IsUnspecified() || aMessageInfo.GetSockAddr().IsMulticast())
     {
-        const Netif::UnicastAddress *source = SelectSourceAddress(aMessageInfo);
+        const Address *source = SelectSourceAddress(aMessageInfo.GetPeerAddr());
 
         VerifyOrExit(source != nullptr, error = kErrorInvalidSourceAddress);
-        header.SetSource(source->GetAddress());
+        header.SetSource(*source);
     }
     else
     {
@@ -511,7 +486,7 @@
         }
 #endif
 
-        SuccessOrExit(error = AddTunneledMplOption(aMessage, header, aMessageInfo));
+        SuccessOrExit(error = AddTunneledMplOption(aMessage, header));
     }
 
     aMessage.SetMulticastLoop(aMessageInfo.GetMulticastLoop());
@@ -530,11 +505,6 @@
     return error;
 }
 
-void Ip6::HandleSendQueue(Tasklet &aTasklet)
-{
-    aTasklet.Get<Ip6>().HandleSendQueue();
-}
-
 void Ip6::HandleSendQueue(void)
 {
     Message *message;
@@ -542,7 +512,7 @@
     while ((message = mSendQueue.GetHead()) != nullptr)
     {
         mSendQueue.Dequeue(*message);
-        IgnoreError(HandleDatagram(*message, nullptr, nullptr, /* aFromHost */ false));
+        IgnoreError(HandleDatagram(*message, kFromHostAllowLoopBack));
     }
 }
 
@@ -550,59 +520,37 @@
 {
     Error          error = kErrorNone;
     HopByHopHeader hbhHeader;
-    OptionHeader   optionHeader;
+    Option         option;
+    uint16_t       offset = aMessage.GetOffset();
     uint16_t       endOffset;
 
-    SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), hbhHeader));
-    endOffset = aMessage.GetOffset() + (hbhHeader.GetLength() + 1) * 8;
+    SuccessOrExit(error = aMessage.Read(offset, hbhHeader));
 
+    endOffset = offset + hbhHeader.GetSize();
     VerifyOrExit(endOffset <= aMessage.GetLength(), error = kErrorParse);
 
-    aMessage.MoveOffset(sizeof(optionHeader));
+    offset += sizeof(HopByHopHeader);
 
-    while (aMessage.GetOffset() < endOffset)
+    for (; offset < endOffset; offset += option.GetSize())
     {
-        SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), optionHeader));
+        SuccessOrExit(error = option.ParseFrom(aMessage, offset, endOffset));
 
-        if (optionHeader.GetType() == OptionPad1::kType)
+        if (option.IsPadding())
         {
-            aMessage.MoveOffset(sizeof(OptionPad1));
             continue;
         }
 
-        VerifyOrExit(aMessage.GetOffset() + sizeof(optionHeader) + optionHeader.GetLength() <= endOffset,
-                     error = kErrorParse);
-
-        switch (optionHeader.GetType())
+        if (option.GetType() == MplOption::kType)
         {
-        case OptionMpl::kType:
-            SuccessOrExit(error = mMpl.ProcessOption(aMessage, aHeader.GetSource(), aIsOutbound, aReceive));
-            break;
-
-        default:
-            switch (optionHeader.GetAction())
-            {
-            case OptionHeader::kActionSkip:
-                break;
-
-            case OptionHeader::kActionDiscard:
-                ExitNow(error = kErrorDrop);
-
-            case OptionHeader::kActionForceIcmp:
-                // TODO: send icmp error
-                ExitNow(error = kErrorDrop);
-
-            case OptionHeader::kActionIcmp:
-                // TODO: send icmp error
-                ExitNow(error = kErrorDrop);
-            }
-
-            break;
+            SuccessOrExit(error = mMpl.ProcessOption(aMessage, offset, aHeader.GetSource(), aIsOutbound, aReceive));
+            continue;
         }
 
-        aMessage.MoveOffset(sizeof(optionHeader) + optionHeader.GetLength());
+        VerifyOrExit(option.GetAction() == Option::kActionSkip, error = kErrorDrop);
     }
 
+    aMessage.SetOffset(offset);
+
 exit:
     return error;
 }
@@ -613,7 +561,7 @@
     Error          error = kErrorNone;
     Header         header;
     FragmentHeader fragmentHeader;
-    Message *      fragment        = nullptr;
+    Message       *fragment        = nullptr;
     uint16_t       fragmentCnt     = 0;
     uint16_t       payloadFragment = 0;
     uint16_t       offset          = 0;
@@ -650,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));
 
@@ -660,10 +608,10 @@
         fragment->SetOffset(aMessage.GetOffset());
         fragment->Write(aMessage.GetOffset(), fragmentHeader);
 
-        VerifyOrExit(aMessage.CopyTo(aMessage.GetOffset() + FragmentHeader::FragmentOffsetToBytes(offset),
-                                     aMessage.GetOffset() + sizeof(fragmentHeader), payloadFragment,
-                                     *fragment) == static_cast<int>(payloadFragment),
-                     error = kErrorNoBufs);
+        fragment->WriteBytesFromMessage(
+            /* aWriteOffset */ aMessage.GetOffset() + sizeof(fragmentHeader), aMessage,
+            /* aReadOffset */ aMessage.GetOffset() + FragmentHeader::FragmentOffsetToBytes(offset),
+            /* aLength */ payloadFragment);
 
         EnqueueDatagram(*fragment);
 
@@ -686,19 +634,16 @@
     return error;
 }
 
-Error Ip6::HandleFragment(Message &aMessage, Netif *aNetif, MessageInfo &aMessageInfo, bool aFromHost)
+Error Ip6::HandleFragment(Message &aMessage, MessageOrigin aOrigin, MessageInfo &aMessageInfo)
 {
     Error          error = kErrorNone;
     Header         header, headerBuffer;
     FragmentHeader fragmentHeader;
-    Message *      message         = nullptr;
+    Message       *message         = nullptr;
     uint16_t       offset          = 0;
     uint16_t       payloadFragment = 0;
-    int            assertValue     = 0;
     bool           isFragmented    = true;
 
-    OT_UNUSED_VARIABLE(assertValue);
-
     SuccessOrExit(error = aMessage.Read(0, header));
     SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), fragmentHeader));
 
@@ -736,17 +681,15 @@
     if (message == nullptr)
     {
         LogDebg("start reassembly");
-        VerifyOrExit((message = NewMessage(0)) != nullptr, error = kErrorNoBufs);
+        VerifyOrExit((message = NewMessage()) != nullptr, error = kErrorNoBufs);
         mReassemblyList.Enqueue(*message);
-        SuccessOrExit(error = message->SetLength(aMessage.GetOffset()));
 
         message->SetTimestampToNow();
         message->SetOffset(0);
         message->SetDatagramTag(fragmentHeader.GetIdentification());
 
         // copying the non-fragmentable header to the fragmentation buffer
-        assertValue = aMessage.CopyTo(0, 0, aMessage.GetOffset(), *message);
-        OT_ASSERT(assertValue == aMessage.GetOffset());
+        SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, 0, aMessage.GetOffset()));
 
         Get<TimeTicker>().RegisterReceiver(TimeTicker::kIp6FragmentReassembler);
     }
@@ -758,9 +701,9 @@
     }
 
     // copy the fragment payload into the message buffer
-    assertValue = aMessage.CopyTo(aMessage.GetOffset() + sizeof(fragmentHeader), aMessage.GetOffset() + offset,
-                                  payloadFragment, *message);
-    OT_ASSERT(assertValue == static_cast<int>(payloadFragment));
+    message->WriteBytesFromMessage(
+        /* aWriteOffset */ aMessage.GetOffset() + offset, aMessage,
+        /* aReadOffset */ aMessage.GetOffset() + sizeof(fragmentHeader), /* aLength */ payloadFragment);
 
     // check if it is the last frame
     if (!fragmentHeader.IsMoreFlagSet())
@@ -778,7 +721,7 @@
 
         mReassemblyList.Dequeue(*message);
 
-        IgnoreError(HandleDatagram(*message, aNetif, aMessageInfo.mLinkInfo, aFromHost));
+        IgnoreError(HandleDatagram(*message, aOrigin, aMessageInfo.mLinkInfo, /* aIsReassembled */ true));
     }
 
 exit:
@@ -801,10 +744,7 @@
     return error;
 }
 
-void Ip6::CleanupFragmentationBuffer(void)
-{
-    mReassemblyList.DequeueAndFreeAll();
-}
+void Ip6::CleanupFragmentationBuffer(void) { mReassemblyList.DequeueAndFreeAll(); }
 
 void Ip6::HandleTimeTick(void)
 {
@@ -865,11 +805,10 @@
     return kErrorNone;
 }
 
-Error Ip6::HandleFragment(Message &aMessage, Netif *aNetif, MessageInfo &aMessageInfo, bool aFromHost)
+Error Ip6::HandleFragment(Message &aMessage, MessageOrigin aOrigin, MessageInfo &aMessageInfo)
 {
-    OT_UNUSED_VARIABLE(aNetif);
+    OT_UNUSED_VARIABLE(aOrigin);
     OT_UNUSED_VARIABLE(aMessageInfo);
-    OT_UNUSED_VARIABLE(aFromHost);
 
     Error          error = kErrorNone;
     FragmentHeader fragmentHeader;
@@ -885,16 +824,15 @@
 }
 #endif // OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE
 
-Error Ip6::HandleExtensionHeaders(Message &    aMessage,
-                                  Netif *      aNetif,
-                                  MessageInfo &aMessageInfo,
-                                  Header &     aHeader,
-                                  uint8_t &    aNextHeader,
-                                  bool         aIsOutbound,
-                                  bool         aFromHost,
-                                  bool &       aReceive)
+Error Ip6::HandleExtensionHeaders(Message      &aMessage,
+                                  MessageOrigin aOrigin,
+                                  MessageInfo  &aMessageInfo,
+                                  Header       &aHeader,
+                                  uint8_t      &aNextHeader,
+                                  bool         &aReceive)
 {
-    Error           error = kErrorNone;
+    Error           error      = kErrorNone;
+    bool            isOutbound = (aOrigin != kFromThreadNetif);
     ExtensionHeader extHeader;
 
     while (aReceive || aNextHeader == kProtoHopOpts)
@@ -904,19 +842,17 @@
         switch (aNextHeader)
         {
         case kProtoHopOpts:
-            SuccessOrExit(error = HandleOptions(aMessage, aHeader, aIsOutbound, aReceive));
+            SuccessOrExit(error = HandleOptions(aMessage, aHeader, isOutbound, aReceive));
             break;
 
         case kProtoFragment:
-#if !OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE
-            IgnoreError(ProcessReceiveCallback(aMessage, aMessageInfo, aNextHeader, aFromHost,
-                                               /* aAllowReceiveFilter */ false, Message::kCopyToUse));
-#endif
-            SuccessOrExit(error = HandleFragment(aMessage, aNetif, aMessageInfo, aFromHost));
+            IgnoreError(PassToHost(aMessage, aOrigin, aMessageInfo, aNextHeader,
+                                   /* aApplyFilter */ false, Message::kCopyToUse));
+            SuccessOrExit(error = HandleFragment(aMessage, aOrigin, aMessageInfo));
             break;
 
         case kProtoDstOpts:
-            SuccessOrExit(error = HandleOptions(aMessage, aHeader, aIsOutbound, aReceive));
+            SuccessOrExit(error = HandleOptions(aMessage, aHeader, isOutbound, aReceive));
             break;
 
         case kProtoIp6:
@@ -937,9 +873,9 @@
     return error;
 }
 
-Error Ip6::HandlePayload(Header &           aIp6Header,
-                         Message &          aMessage,
-                         MessageInfo &      aMessageInfo,
+Error Ip6::HandlePayload(Header            &aIp6Header,
+                         Message           &aMessage,
+                         MessageInfo       &aMessageInfo,
                          uint8_t            aIpProto,
                          Message::Ownership aMessageOwnership)
 {
@@ -950,7 +886,18 @@
     Error    error   = kErrorNone;
     Message *message = (aMessageOwnership == Message::kTakeCustody) ? &aMessage : nullptr;
 
-    VerifyOrExit(aIpProto == kProtoTcp || aIpProto == kProtoUdp || aIpProto == kProtoIcmp6);
+    switch (aIpProto)
+    {
+    case kProtoUdp:
+    case kProtoIcmp6:
+        break;
+#if OPENTHREAD_CONFIG_TCP_ENABLE
+    case kProtoTcp:
+        break;
+#endif
+    default:
+        ExitNow();
+    }
 
     if (aMessageOwnership == Message::kCopyToUse)
     {
@@ -995,26 +942,47 @@
     return error;
 }
 
-Error Ip6::ProcessReceiveCallback(Message &          aMessage,
-                                  const MessageInfo &aMessageInfo,
-                                  uint8_t            aIpProto,
-                                  bool               aFromHost,
-                                  bool               aAllowReceiveFilter,
-                                  Message::Ownership aMessageOwnership)
+Error Ip6::PassToHost(Message           &aMessage,
+                      MessageOrigin      aOrigin,
+                      const MessageInfo &aMessageInfo,
+                      uint8_t            aIpProto,
+                      bool               aApplyFilter,
+                      Message::Ownership aMessageOwnership)
 {
+    // This method passes the message to host by invoking the
+    // registered IPv6 receive callback. When NAT64 is enabled, it
+    // may also perform translation and invoke IPv4 receive
+    // callback.
+
     Error    error   = kErrorNone;
-    Message *message = &aMessage;
+    Message *message = nullptr;
 
-    VerifyOrExit(!aFromHost, error = kErrorNoRoute);
-    VerifyOrExit(mReceiveIp6DatagramCallback != nullptr, error = kErrorNoRoute);
+    // `message` points to the `Message` instance we own in this
+    // method. If we can take ownership of `aMessage`, we use it as
+    // `message`. Otherwise, we may create a clone of it and use as
+    // `message`. `message` variable will be set to `nullptr` if the
+    // message ownership is transferred to an invoked callback. At
+    // the end of this method we free `message` if it is not `nullptr`
+    // indicating it was not passed to a callback.
 
-    // Do not forward reassembled IPv6 packets.
+    if (aMessageOwnership == Message::kTakeCustody)
+    {
+        message = &aMessage;
+    }
+
+    VerifyOrExit(aOrigin != kFromHostDisallowLoopBack, error = kErrorNoRoute);
+
+    VerifyOrExit(mReceiveIp6DatagramCallback.IsSet(), error = kErrorNoRoute);
+
+    // Do not pass IPv6 packets that exceed kMinimalMtu.
     VerifyOrExit(aMessage.GetLength() <= kMinimalMtu, error = kErrorDrop);
 
-    if (mIsReceiveIp6FilterEnabled && aAllowReceiveFilter)
+    if (mIsReceiveIp6FilterEnabled && aApplyFilter)
     {
 #if !OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE
-        // do not pass messages sent to an RLOC/ALOC, except Service Locator
+        // Do not pass messages sent to an RLOC/ALOC, except
+        // Service Locator
+
         bool isLocator = Get<Mle::Mle>().IsMeshLocalAddress(aMessageInfo.GetSockAddr()) &&
                          aMessageInfo.GetSockAddr().GetIid().IsLocator();
 
@@ -1028,9 +996,8 @@
             if (mIcmp.ShouldHandleEchoRequest(aMessageInfo))
             {
                 Icmp::Header icmp;
-                IgnoreError(aMessage.Read(aMessage.GetOffset(), icmp));
 
-                // do not pass ICMP Echo Request messages
+                IgnoreError(aMessage.Read(aMessage.GetOffset(), icmp));
                 VerifyOrExit(icmp.GetType() != Icmp::Header::kTypeEchoRequest, error = kErrorDrop);
             }
 
@@ -1047,6 +1014,14 @@
             break;
         }
 
+#if OPENTHREAD_CONFIG_TCP_ENABLE
+        // Do not pass TCP message to avoid dual processing from both
+        // OpenThread and POSIX TCP stacks.
+        case kProtoTcp:
+            error = kErrorNoRoute;
+            ExitNow();
+#endif
+
         default:
             break;
         }
@@ -1070,40 +1045,64 @@
     }
 
     IgnoreError(RemoveMplOption(*message));
-    mReceiveIp6DatagramCallback(message, mReceiveIp6DatagramCallbackContext);
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    switch (Get<Nat64::Translator>().TranslateFromIp6(aMessage))
+    {
+    case Nat64::Translator::kNotTranslated:
+        break;
+
+    case Nat64::Translator::kDrop:
+        ExitNow(error = kErrorDrop);
+
+    case Nat64::Translator::kForward:
+        VerifyOrExit(mReceiveIp4DatagramCallback.IsSet(), error = kErrorNoRoute);
+        // Pass message to callback transferring its ownership.
+        mReceiveIp4DatagramCallback.Invoke(message);
+        message = nullptr;
+        ExitNow();
+    }
+#endif
+
+    // Pass message to callback transferring its ownership.
+    mReceiveIp6DatagramCallback.Invoke(message);
+    message = nullptr;
+
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+    {
+        Header header;
+
+        IgnoreError(header.ParseFrom(aMessage));
+        UpdateBorderRoutingCounters(header, aMessage.GetLength(), /* aIsInbound */ false);
+    }
+#endif
 
 exit:
-
-    if ((error != kErrorNone) && (aMessageOwnership == Message::kTakeCustody))
-    {
-        aMessage.Free();
-    }
-
+    FreeMessage(message);
     return error;
 }
 
-Error Ip6::SendRaw(Message &aMessage, bool aFromHost)
+Error Ip6::SendRaw(Message &aMessage, bool aAllowLoopBackToHost)
 {
-    Error       error = kErrorNone;
-    Header      header;
-    MessageInfo messageInfo;
-    bool        freed = false;
+    Error  error = kErrorNone;
+    Header header;
+    bool   freed = false;
 
     SuccessOrExit(error = header.ParseFrom(aMessage));
     VerifyOrExit(!header.GetSource().IsMulticast(), error = kErrorInvalidSourceAddress);
 
-    messageInfo.SetPeerAddr(header.GetSource());
-    messageInfo.SetSockAddr(header.GetDestination());
-    messageInfo.SetHopLimit(header.GetHopLimit());
-
     if (header.GetDestination().IsMulticast())
     {
-        SuccessOrExit(error = InsertMplOption(aMessage, header, messageInfo));
+        SuccessOrExit(error = InsertMplOption(aMessage, header));
     }
 
-    error = HandleDatagram(aMessage, nullptr, nullptr, aFromHost);
+    error = HandleDatagram(aMessage, aAllowLoopBackToHost ? kFromHostAllowLoopBack : kFromHostDisallowLoopBack);
     freed = true;
 
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+    UpdateBorderRoutingCounters(header, aMessage.GetLength(), /* aIsInbound */ true);
+#endif
+
 exit:
 
     if (!freed)
@@ -1114,7 +1113,7 @@
     return error;
 }
 
-Error Ip6::HandleDatagram(Message &aMessage, Netif *aNetif, const void *aLinkMessageInfo, bool aFromHost)
+Error Ip6::HandleDatagram(Message &aMessage, MessageOrigin aOrigin, const void *aLinkMessageInfo, bool aIsReassembled)
 {
     Error       error;
     MessageInfo messageInfo;
@@ -1140,100 +1139,99 @@
     messageInfo.SetEcn(header.GetEcn());
     messageInfo.SetLinkInfo(aLinkMessageInfo);
 
-    // determine destination of packet
+    // Determine `forwardThread`, `forwardHost` and `receive`
+    // based on the destination address.
+
     if (header.GetDestination().IsMulticast())
     {
-        Netif *netif;
+        // Destination is multicast
 
-        if (aNetif != nullptr)
-        {
+        forwardThread = (aOrigin != kFromThreadNetif);
+
 #if OPENTHREAD_FTD
-            if (header.GetDestination().IsMulticastLargerThanRealmLocal() &&
-                Get<ChildTable>().HasSleepyChildWithAddress(header.GetDestination()))
-            {
-                forwardThread = true;
-            }
-#endif
-
-            netif = aNetif;
-        }
-        else
+        if ((aOrigin == kFromThreadNetif) && header.GetDestination().IsMulticastLargerThanRealmLocal() &&
+            Get<ChildTable>().HasSleepyChildWithAddress(header.GetDestination()))
         {
             forwardThread = true;
-
-            netif = &Get<ThreadNetif>();
         }
+#endif
 
         forwardHost = header.GetDestination().IsMulticastLargerThanRealmLocal();
 
-        if ((aNetif != nullptr || aMessage.GetMulticastLoop()) && netif->IsMulticastSubscribed(header.GetDestination()))
+        if (((aOrigin == kFromThreadNetif) || aMessage.GetMulticastLoop()) &&
+            Get<ThreadNetif>().IsMulticastSubscribed(header.GetDestination()))
         {
             receive = true;
         }
-        else if (netif->IsMulticastPromiscuousEnabled())
+        else if (Get<ThreadNetif>().IsMulticastPromiscuousEnabled())
         {
             forwardHost = true;
         }
     }
     else
     {
-        // unicast
+        // Destination is unicast
+
         if (Get<ThreadNetif>().HasUnicastAddress(header.GetDestination()))
         {
             receive = true;
         }
-        else if (!header.GetDestination().IsLinkLocal())
+        else if ((aOrigin != kFromThreadNetif) || !header.GetDestination().IsLinkLocal())
         {
-            forwardThread = true;
-        }
-        else if (aNetif == nullptr)
-        {
-            forwardThread = true;
-        }
+            if (header.GetDestination().IsLinkLocal())
+            {
+                forwardThread = true;
+            }
+            else if (IsOnLink(header.GetDestination()))
+            {
+#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
+                forwardThread = ((aOrigin == kFromHostDisallowLoopBack) ||
+                                 !Get<BackboneRouter::Manager>().ShouldForwardDuaToBackbone(header.GetDestination()));
+#else
+                forwardThread = true;
+#endif
+            }
+            else if (RouteLookup(header.GetSource(), header.GetDestination()) == kErrorNone)
+            {
+                forwardThread = true;
+            }
 
-        if (forwardThread && !ShouldForwardToThread(messageInfo, aFromHost))
-        {
-            forwardThread = false;
-            forwardHost   = true;
+            forwardHost = !forwardThread;
         }
     }
 
     aMessage.SetOffset(sizeof(header));
 
-    // process IPv6 Extension Headers
+    // Process IPv6 Extension Headers
     nextHeader = static_cast<uint8_t>(header.GetNextHeader());
-    SuccessOrExit(error = HandleExtensionHeaders(aMessage, aNetif, messageInfo, header, nextHeader, aNetif == nullptr,
-                                                 aFromHost, receive));
+    SuccessOrExit(error = HandleExtensionHeaders(aMessage, aOrigin, messageInfo, header, nextHeader, receive));
 
-    // process IPv6 Payload
-    if (receive)
+    if (receive && (nextHeader == kProtoIp6))
     {
-        if (nextHeader == kProtoIp6)
-        {
-            // Remove encapsulating header and start over.
-            aMessage.RemoveHeader(aMessage.GetOffset());
-            Get<MeshForwarder>().LogMessage(MeshForwarder::kMessageReceive, aMessage);
-            goto start;
-        }
-
-        error = ProcessReceiveCallback(aMessage, messageInfo, nextHeader, aFromHost,
-                                       /* aAllowReceiveFilter */ !forwardHost, Message::kCopyToUse);
-
-        if ((error == kErrorNone || error == kErrorNoRoute) && forwardHost)
-        {
-            forwardHost = false;
-        }
-
-        error             = HandlePayload(header, aMessage, messageInfo, nextHeader,
-                              (forwardThread || forwardHost ? Message::kCopyToUse : Message::kTakeCustody));
-        shouldFreeMessage = forwardThread || forwardHost;
+        // Remove encapsulating header and start over.
+        aMessage.RemoveHeader(aMessage.GetOffset());
+        Get<MeshForwarder>().LogMessage(MeshForwarder::kMessageReceive, aMessage);
+        goto start;
     }
 
-    if (forwardHost)
+    if ((forwardHost || receive) && !aIsReassembled)
     {
-        // try passing to host
-        error = ProcessReceiveCallback(aMessage, messageInfo, nextHeader, aFromHost, /* aAllowReceiveFilter */ false,
-                                       forwardThread ? Message::kCopyToUse : Message::kTakeCustody);
+        error = PassToHost(aMessage, aOrigin, messageInfo, nextHeader,
+                           /* aApplyFilter */ !forwardHost,
+                           (receive || forwardThread) ? Message::kCopyToUse : Message::kTakeCustody);
+
+        // Need to free the message if we did not pass its
+        // ownership in the call to `PassToHost()`
+        shouldFreeMessage = (receive || forwardThread);
+    }
+
+    if (receive)
+    {
+        error = HandlePayload(header, aMessage, messageInfo, nextHeader,
+                              forwardThread ? Message::kCopyToUse : Message::kTakeCustody);
+
+        // Need to free the message if we did not pass its
+        // ownership in the call to `HandlePayload()`
         shouldFreeMessage = forwardThread;
     }
 
@@ -1241,9 +1239,9 @@
     {
         uint8_t hopLimit;
 
-        if (aNetif != nullptr)
+        if (aOrigin == kFromThreadNetif)
         {
-            VerifyOrExit(mForwardingEnabled);
+            VerifyOrExit(Get<Mle::Mle>().IsRouterOrLeader());
             header.SetHopLimit(header.GetHopLimit() - 1);
         }
 
@@ -1270,7 +1268,7 @@
         }
 
 #if !OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-        if (aFromHost && (nextHeader == kProtoUdp))
+        if ((aOrigin == kFromHostDisallowLoopBack) && (nextHeader == kProtoUdp))
         {
             uint16_t destPort;
 
@@ -1293,7 +1291,7 @@
 #endif
 
         // `SendMessage()` takes custody of message in the success case
-        SuccessOrExit(error = Get<ThreadNetif>().SendMessage(aMessage));
+        SuccessOrExit(error = Get<MeshForwarder>().SendMessage(aMessage));
         shouldFreeMessage = false;
     }
 
@@ -1307,122 +1305,105 @@
     return error;
 }
 
-bool Ip6::ShouldForwardToThread(const MessageInfo &aMessageInfo, bool aFromHost) const
+Error Ip6::SelectSourceAddress(MessageInfo &aMessageInfo) const
 {
-    bool shouldForward = false;
+    Error          error = kErrorNone;
+    const Address *source;
 
-    if (aMessageInfo.GetSockAddr().IsMulticast() || aMessageInfo.GetSockAddr().IsLinkLocal())
-    {
-        shouldForward = true;
-    }
-    else if (IsOnLink(aMessageInfo.GetSockAddr()))
-    {
-#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
-        shouldForward =
-            (aFromHost || !Get<BackboneRouter::Manager>().ShouldForwardDuaToBackbone(aMessageInfo.GetSockAddr()));
-#else
-        OT_UNUSED_VARIABLE(aFromHost);
-        shouldForward = true;
-#endif
-    }
-    else if (Get<ThreadNetif>().RouteLookup(aMessageInfo.GetPeerAddr(), aMessageInfo.GetSockAddr(), nullptr) ==
-             kErrorNone)
-    {
-        shouldForward = true;
-    }
+    source = SelectSourceAddress(aMessageInfo.GetPeerAddr());
+    VerifyOrExit(source != nullptr, error = kErrorNotFound);
+    aMessageInfo.SetSockAddr(*source);
 
-    return shouldForward;
+exit:
+    return error;
 }
 
-const Netif::UnicastAddress *Ip6::SelectSourceAddress(MessageInfo &aMessageInfo)
+const Address *Ip6::SelectSourceAddress(const Address &aDestination) const
 {
-    Address *                    destination                 = &aMessageInfo.GetPeerAddr();
-    uint8_t                      destinationScope            = destination->GetScope();
-    const bool                   destinationIsRoutingLocator = Get<Mle::Mle>().IsRoutingLocator(*destination);
-    const Netif::UnicastAddress *rvalAddr                    = nullptr;
-    uint8_t                      rvalPrefixMatched           = 0;
+    uint8_t                      destScope    = aDestination.GetScope();
+    bool                         destIsRloc   = Get<Mle::Mle>().IsRoutingLocator(aDestination);
+    const Netif::UnicastAddress *bestAddr     = nullptr;
+    uint8_t                      bestMatchLen = 0;
 
     for (const Netif::UnicastAddress &addr : Get<ThreadNetif>().GetUnicastAddresses())
     {
-        const Address *candidateAddr = &addr.GetAddress();
-        uint8_t        candidatePrefixMatched;
-        uint8_t        overrideScope;
+        uint8_t matchLen;
+        uint8_t overrideScope;
 
-        if (Get<Mle::Mle>().IsAnycastLocator(*candidateAddr))
+        if (Get<Mle::Mle>().IsAnycastLocator(addr.GetAddress()))
         {
             // Don't use anycast address as source address.
             continue;
         }
 
-        candidatePrefixMatched = destination->PrefixMatch(*candidateAddr);
+        matchLen = aDestination.PrefixMatch(addr.GetAddress());
 
-        if (candidatePrefixMatched >= addr.mPrefixLength)
+        if (matchLen >= addr.mPrefixLength)
         {
-            candidatePrefixMatched = addr.mPrefixLength;
-            overrideScope          = addr.GetScope();
+            matchLen      = addr.mPrefixLength;
+            overrideScope = addr.GetScope();
         }
         else
         {
-            overrideScope = destinationScope;
+            overrideScope = destScope;
         }
 
-        if (rvalAddr == nullptr)
+        if (bestAddr == nullptr)
         {
             // Rule 0: Prefer any address
-            rvalAddr          = &addr;
-            rvalPrefixMatched = candidatePrefixMatched;
+            bestAddr     = &addr;
+            bestMatchLen = matchLen;
         }
-        else if (*candidateAddr == *destination)
+        else if (addr.GetAddress() == aDestination)
         {
             // Rule 1: Prefer same address
-            rvalAddr = &addr;
+            bestAddr = &addr;
             ExitNow();
         }
-        else if (addr.GetScope() < rvalAddr->GetScope())
+        else if (addr.GetScope() < bestAddr->GetScope())
         {
             // Rule 2: Prefer appropriate scope
             if (addr.GetScope() >= overrideScope)
             {
-                rvalAddr          = &addr;
-                rvalPrefixMatched = candidatePrefixMatched;
+                bestAddr     = &addr;
+                bestMatchLen = matchLen;
             }
             else
             {
                 continue;
             }
         }
-        else if (addr.GetScope() > rvalAddr->GetScope())
+        else if (addr.GetScope() > bestAddr->GetScope())
         {
-            if (rvalAddr->GetScope() < overrideScope)
+            if (bestAddr->GetScope() < overrideScope)
             {
-                rvalAddr          = &addr;
-                rvalPrefixMatched = candidatePrefixMatched;
+                bestAddr     = &addr;
+                bestMatchLen = matchLen;
             }
             else
             {
                 continue;
             }
         }
-        else if (addr.mPreferred && !rvalAddr->mPreferred)
+        else if (addr.mPreferred && !bestAddr->mPreferred)
         {
             // Rule 3: Avoid deprecated addresses
-            rvalAddr          = &addr;
-            rvalPrefixMatched = candidatePrefixMatched;
+            bestAddr     = &addr;
+            bestMatchLen = matchLen;
         }
-        else if (candidatePrefixMatched > rvalPrefixMatched)
+        else if (matchLen > bestMatchLen)
         {
             // Rule 6: Prefer matching label
             // Rule 7: Prefer public address
             // Rule 8: Use longest prefix matching
-            rvalAddr          = &addr;
-            rvalPrefixMatched = candidatePrefixMatched;
+            bestAddr     = &addr;
+            bestMatchLen = matchLen;
         }
-        else if ((candidatePrefixMatched == rvalPrefixMatched) &&
-                 (destinationIsRoutingLocator == Get<Mle::Mle>().IsRoutingLocator(*candidateAddr)))
+        else if ((matchLen == bestMatchLen) && (destIsRloc == Get<Mle::Mle>().IsRoutingLocator(addr.GetAddress())))
         {
             // Additional rule: Prefer RLOC source for RLOC destination, EID source for anything else
-            rvalAddr          = &addr;
-            rvalPrefixMatched = candidatePrefixMatched;
+            bestAddr     = &addr;
+            bestMatchLen = matchLen;
         }
         else
         {
@@ -1430,37 +1411,108 @@
         }
 
         // infer destination scope based on prefix match
-        if (rvalPrefixMatched >= rvalAddr->mPrefixLength)
+        if (bestMatchLen >= bestAddr->mPrefixLength)
         {
-            destinationScope = rvalAddr->GetScope();
+            destScope = bestAddr->GetScope();
         }
     }
 
 exit:
-    return rvalAddr;
+    return (bestAddr != nullptr) ? &bestAddr->GetAddress() : nullptr;
 }
 
 bool Ip6::IsOnLink(const Address &aAddress) const
 {
-    bool rval = false;
+    bool isOnLink = false;
 
-    if (Get<ThreadNetif>().IsOnMesh(aAddress))
+    if (Get<NetworkData::Leader>().IsOnMesh(aAddress))
     {
-        ExitNow(rval = true);
+        ExitNow(isOnLink = true);
     }
 
-    for (const Netif::UnicastAddress &cur : Get<ThreadNetif>().GetUnicastAddresses())
+    for (const Netif::UnicastAddress &unicastAddr : Get<ThreadNetif>().GetUnicastAddresses())
     {
-        if (cur.GetAddress().PrefixMatch(aAddress) >= cur.mPrefixLength)
+        if (unicastAddr.GetAddress().PrefixMatch(aAddress) >= unicastAddr.mPrefixLength)
         {
-            ExitNow(rval = true);
+            ExitNow(isOnLink = true);
         }
     }
 
 exit:
-    return rval;
+    return isOnLink;
 }
 
+Error Ip6::RouteLookup(const Address &aSource, const Address &aDestination) const
+{
+    Error    error;
+    uint16_t rloc;
+
+    error = Get<NetworkData::Leader>().RouteLookup(aSource, aDestination, rloc);
+
+    if (error == kErrorNone)
+    {
+        if (rloc == Get<Mle::MleRouter>().GetRloc16())
+        {
+            error = kErrorNoRoute;
+        }
+    }
+    else
+    {
+        LogNote("Failed to find valid route for: %s", aDestination.ToString().AsCString());
+    }
+
+    return error;
+}
+
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+void Ip6::UpdateBorderRoutingCounters(const Header &aHeader, uint16_t aMessageLength, bool aIsInbound)
+{
+    otPacketsAndBytes *counter = nullptr;
+
+    VerifyOrExit(!aHeader.GetSource().IsLinkLocal());
+    VerifyOrExit(!aHeader.GetDestination().IsLinkLocal());
+    VerifyOrExit(aHeader.GetSource().GetPrefix() != Get<Mle::Mle>().GetMeshLocalPrefix());
+    VerifyOrExit(aHeader.GetDestination().GetPrefix() != Get<Mle::Mle>().GetMeshLocalPrefix());
+
+    if (aIsInbound)
+    {
+        VerifyOrExit(!Get<Netif>().HasUnicastAddress(aHeader.GetSource()));
+
+        if (aHeader.GetDestination().IsMulticast())
+        {
+            VerifyOrExit(aHeader.GetDestination().IsMulticastLargerThanRealmLocal());
+            counter = &mBorderRoutingCounters.mInboundMulticast;
+        }
+        else
+        {
+            counter = &mBorderRoutingCounters.mInboundUnicast;
+        }
+    }
+    else
+    {
+        VerifyOrExit(!Get<Netif>().HasUnicastAddress(aHeader.GetDestination()));
+
+        if (aHeader.GetDestination().IsMulticast())
+        {
+            VerifyOrExit(aHeader.GetDestination().IsMulticastLargerThanRealmLocal());
+            counter = &mBorderRoutingCounters.mOutboundMulticast;
+        }
+        else
+        {
+            counter = &mBorderRoutingCounters.mOutboundUnicast;
+        }
+    }
+
+exit:
+
+    if (counter)
+    {
+        counter->mPackets += 1;
+        counter->mBytes += aMessageLength;
+    }
+}
+#endif
+
 // LCOV_EXCL_START
 
 const char *Ip6::IpProtoToString(uint8_t aIpProto)
@@ -1527,12 +1579,9 @@
     return error;
 }
 
-Error Headers::DecompressFrom(const Message &     aMessage,
-                              uint16_t            aOffset,
-                              const Mac::Address &aMacSource,
-                              const Mac::Address &aMacDest)
+Error Headers::DecompressFrom(const Message &aMessage, uint16_t aOffset, const Mac::Addresses &aMacAddrs)
 {
-    static constexpr uint16_t kReadLength = Lowpan::FragmentHeader::kSubsequentFragmentHeaderSize + sizeof(Headers);
+    static constexpr uint16_t kReadLength = sizeof(Lowpan::FragmentHeader::NextFrag) + sizeof(Headers);
 
     uint8_t   frameBuffer[kReadLength];
     uint16_t  frameLength;
@@ -1541,13 +1590,10 @@
     frameLength = aMessage.ReadBytes(aOffset, frameBuffer, sizeof(frameBuffer));
     frameData.Init(frameBuffer, frameLength);
 
-    return DecompressFrom(frameData, aMacSource, aMacDest, aMessage.GetInstance());
+    return DecompressFrom(frameData, aMacAddrs, aMessage.GetInstance());
 }
 
-Error Headers::DecompressFrom(const FrameData &   aFrameData,
-                              const Mac::Address &aMacSource,
-                              const Mac::Address &aMacDest,
-                              Instance &          aInstance)
+Error Headers::DecompressFrom(const FrameData &aFrameData, const Mac::Addresses &aMacAddrs, Instance &aInstance)
 {
     Error                  error     = kErrorNone;
     FrameData              frameData = aFrameData;
@@ -1563,7 +1609,7 @@
     VerifyOrExit(Lowpan::Lowpan::IsLowpanHc(frameData), error = kErrorNotFound);
 
     SuccessOrExit(error = aInstance.Get<Lowpan::Lowpan>().DecompressBaseHeader(mIp6Header, nextHeaderCompressed,
-                                                                               aMacSource, aMacDest, frameData));
+                                                                               aMacAddrs, frameData));
 
     switch (mIp6Header.GetNextHeader())
     {
diff --git a/src/core/net/ip6.hpp b/src/core/net/ip6.hpp
index 5e695cc..aafb09c 100644
--- a/src/core/net/ip6.hpp
+++ b/src/core/net/ip6.hpp
@@ -39,8 +39,10 @@
 #include <stddef.h>
 
 #include <openthread/ip6.h>
+#include <openthread/nat64.h>
 #include <openthread/udp.h>
 
+#include "common/callback.hpp"
 #include "common/encoding.hpp"
 #include "common/frame_data.hpp"
 #include "common/locator.hpp"
@@ -112,6 +114,20 @@
 
 public:
     /**
+     * This enumeration represents an IPv6 message origin.
+     *
+     * In case the message is originating from host, it also indicates whether or not it is allowed to passed back the
+     * message to the host.
+     *
+     */
+    enum MessageOrigin : uint8_t
+    {
+        kFromThreadNetif,          ///< Message originates from Thread Netif.
+        kFromHostDisallowLoopBack, ///< Message originates from host and should not be passed back to host.
+        kFromHostAllowLoopBack,    ///< Message originates from host and can be passed back to host.
+    };
+
+    /**
      * This constructor initializes the object.
      *
      * @param[in]  aInstance   A reference to the otInstance object.
@@ -120,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.
@@ -128,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.
@@ -186,8 +211,8 @@
      * The caller transfers ownership of @p aMessage when making this call. OpenThread will free @p aMessage when
      * processing is complete, including when a value other than `kErrorNone` is returned.
      *
-     * @param[in]  aMessage          A reference to the message.
-     * @param[in]  aFromHost         TRUE if the message is originated from the host, FALSE otherwise.
+     * @param[in]  aMessage               A reference to the message.
+     * @param[in]  aAllowLoopBackToHost   Indicate whether or not the message is allowed to be passed back to host.
      *
      * @retval kErrorNone     Successfully processed the message.
      * @retval kErrorDrop     Message was well-formed but not fully processed due to packet processing rules.
@@ -196,15 +221,14 @@
      * @retval kErrorParse    Encountered a malformed header when processing the message.
      *
      */
-    Error SendRaw(Message &aMessage, bool aFromHost);
+    Error SendRaw(Message &aMessage, bool aAllowLoopBackToHost);
 
     /**
      * This method processes a received IPv6 datagram.
      *
      * @param[in]  aMessage          A reference to the message.
-     * @param[in]  aNetif            A pointer to the network interface that received the message.
+     * @param[in]  aOrigin           The message oirgin.
      * @param[in]  aLinkMessageInfo  A pointer to link-specific message information.
-     * @param[in]  aFromHost         TRUE if the message is originated from the host, FALSE otherwise.
      *
      * @retval kErrorNone     Successfully processed the message.
      * @retval kErrorDrop     Message was well-formed but not fully processed due to packet processing rules.
@@ -213,7 +237,10 @@
      * @retval kErrorParse    Encountered a malformed header when processing the message.
      *
      */
-    Error HandleDatagram(Message &aMessage, Netif *aNetif, const void *aLinkMessageInfo, bool aFromHost);
+    Error HandleDatagram(Message      &aMessage,
+                         MessageOrigin aOrigin,
+                         const void   *aLinkMessageInfo = nullptr,
+                         bool          aIsReassembled   = false);
 
     /**
      * This method registers a callback to provide received raw IPv6 datagrams.
@@ -229,7 +256,27 @@
      * @sa SetReceiveIp6FilterEnabled
      *
      */
-    void SetReceiveDatagramCallback(otIp6ReceiveCallback aCallback, void *aCallbackContext);
+    void SetReceiveDatagramCallback(otIp6ReceiveCallback aCallback, void *aCallbackContext)
+    {
+        mReceiveIp6DatagramCallback.Set(aCallback, aCallbackContext);
+    }
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    /**
+     * This method registers a callback to provide received translated IPv4 datagrams.
+     *
+     * @param[in]  aCallback         A pointer to a function that is called when a translated IPv4 datagram is received
+     *                               or `nullptr` to disable the callback.
+     * @param[in]  aCallbackContext  A pointer to application-specific context.
+     *
+     * @sa SetReceiveDatagramCallback
+     *
+     */
+    void SetNat64ReceiveIp4DatagramCallback(otNat64ReceiveIp4Callback aCallback, void *aCallbackContext)
+    {
+        mReceiveIp4DatagramCallback.Set(aCallback, aCallbackContext);
+    }
+#endif
 
     /**
      * This method indicates whether or not Thread control traffic is filtered out when delivering IPv6 datagrams
@@ -256,30 +303,25 @@
     void SetReceiveIp6FilterEnabled(bool aEnabled) { mIsReceiveIp6FilterEnabled = aEnabled; }
 
     /**
-     * This method indicates whether or not IPv6 forwarding is enabled.
+     * This method performs default source address selection.
      *
-     * @returns TRUE if IPv6 forwarding is enabled, FALSE otherwise.
+     * @param[in,out]  aMessageInfo  A reference to the message information.
+     *
+     * @retval  kErrorNone      Found a source address and updated SockAddr of @p aMessageInfo.
+     * @retval  kErrorNotFound  No source address was found and @p aMessageInfo is unchanged.
      *
      */
-    bool IsForwardingEnabled(void) const { return mForwardingEnabled; }
+    Error SelectSourceAddress(MessageInfo &aMessageInfo) const;
 
     /**
-     * This method enables/disables IPv6 forwarding.
+     * This method performs default source address selection.
      *
-     * @param[in]  aEnable  TRUE to enable IPv6 forwarding, FALSE otherwise.
-     *
-     */
-    void SetForwardingEnabled(bool aEnable) { mForwardingEnabled = aEnable; }
-
-    /**
-     * This method perform default source address selection.
-     *
-     * @param[in]  aMessageInfo  A reference to the message information.
+     * @param[in]  aDestination  The destination address.
      *
      * @returns A pointer to the selected IPv6 source address or `nullptr` if no source address was found.
      *
      */
-    const Netif::UnicastAddress *SelectSourceAddress(MessageInfo &aMessageInfo);
+    const Address *SelectSourceAddress(const Address &aDestination) const;
 
     /**
      * This method returns a reference to the send queue.
@@ -309,35 +351,55 @@
      */
     static const char *EcnToString(Ecn aEcn);
 
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+    /**
+     * This method returns a reference to the Border Routing counters.
+     *
+     * @returns A reference to the Border Routing counters.
+     *
+     */
+    const otBorderRoutingCounters &GetBorderRoutingCounters(void) const { return mBorderRoutingCounters; }
+
+    /**
+     * This method returns a reference to the Border Routing counters.
+     *
+     * @returns A reference to the Border Routing counters.
+     *
+     */
+    otBorderRoutingCounters &GetBorderRoutingCounters(void) { return mBorderRoutingCounters; }
+
+    /**
+     * This method resets the Border Routing counters.
+     *
+     */
+    void ResetBorderRoutingCounters(void) { memset(&mBorderRoutingCounters, 0, sizeof(mBorderRoutingCounters)); }
+#endif
+
 private:
     static constexpr uint8_t kDefaultHopLimit      = OPENTHREAD_CONFIG_IP6_HOP_LIMIT_DEFAULT;
     static constexpr uint8_t kIp6ReassemblyTimeout = OPENTHREAD_CONFIG_IP6_REASSEMBLY_TIMEOUT;
 
     static constexpr uint16_t kMinimalMtu = 1280;
 
-    static void HandleSendQueue(Tasklet &aTasklet);
-    void        HandleSendQueue(void);
+    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 ProcessReceiveCallback(Message &          aMessage,
-                                 const MessageInfo &aMessageInfo,
-                                 uint8_t            aIpProto,
-                                 bool               aFromHost,
-                                 bool               aAllowReceiveFilter,
-                                 Message::Ownership aMessageOwnership);
-    Error HandleExtensionHeaders(Message &    aMessage,
-                                 Netif *      aNetif,
-                                 MessageInfo &aMessageInfo,
-                                 Header &     aHeader,
-                                 uint8_t &    aNextHeader,
-                                 bool         aIsOutbound,
-                                 bool         aFromHost,
-                                 bool &       aReceive);
+    Error PassToHost(Message           &aMessage,
+                     MessageOrigin      aOrigin,
+                     const MessageInfo &aMessageInfo,
+                     uint8_t            aIpProto,
+                     bool               aApplyFilter,
+                     Message::Ownership aMessageOwnership);
+    Error HandleExtensionHeaders(Message      &aMessage,
+                                 MessageOrigin aOrigin,
+                                 MessageInfo  &aMessageInfo,
+                                 Header       &aHeader,
+                                 uint8_t      &aNextHeader,
+                                 bool         &aReceive);
     Error FragmentDatagram(Message &aMessage, uint8_t aIpProto);
-    Error HandleFragment(Message &aMessage, Netif *aNetif, MessageInfo &aMessageInfo, bool aFromHost);
+    Error HandleFragment(Message &aMessage, MessageOrigin aOrigin, MessageInfo &aMessageInfo);
 #if OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE
     void CleanupFragmentationBuffer(void);
     void HandleTimeTick(void);
@@ -345,25 +407,33 @@
     void SendIcmpError(Message &aMessage, Icmp::Header::Type aIcmpType, Icmp::Header::Code aIcmpCode);
 #endif
     Error AddMplOption(Message &aMessage, Header &aHeader);
-    Error AddTunneledMplOption(Message &aMessage, Header &aHeader, MessageInfo &aMessageInfo);
-    Error InsertMplOption(Message &aMessage, Header &aHeader, MessageInfo &aMessageInfo);
+    Error AddTunneledMplOption(Message &aMessage, Header &aHeader);
+    Error InsertMplOption(Message &aMessage, Header &aHeader);
     Error RemoveMplOption(Message &aMessage);
     Error HandleOptions(Message &aMessage, Header &aHeader, bool aIsOutbound, bool &aReceive);
-    Error HandlePayload(Header &           aIp6Header,
-                        Message &          aMessage,
-                        MessageInfo &      aMessageInfo,
+    Error HandlePayload(Header            &aIp6Header,
+                        Message           &aMessage,
+                        MessageInfo       &aMessageInfo,
                         uint8_t            aIpProto,
                         Message::Ownership aMessageOwnership);
-    bool  ShouldForwardToThread(const MessageInfo &aMessageInfo, bool aFromHost) const;
     bool  IsOnLink(const Address &aAddress) const;
+    Error RouteLookup(const Address &aSource, const Address &aDestination) const;
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+    void UpdateBorderRoutingCounters(const Header &aHeader, uint16_t aMessageLength, bool aIsInbound);
+#endif
 
-    bool                 mForwardingEnabled;
-    bool                 mIsReceiveIp6FilterEnabled;
-    otIp6ReceiveCallback mReceiveIp6DatagramCallback;
-    void *               mReceiveIp6DatagramCallbackContext;
+    using SendQueueTask = TaskletIn<Ip6, &Ip6::HandleSendQueue>;
+
+    bool mIsReceiveIp6FilterEnabled;
+
+    Callback<otIp6ReceiveCallback> mReceiveIp6DatagramCallback;
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    Callback<otNat64ReceiveIp4Callback> mReceiveIp4DatagramCallback;
+#endif
 
     PriorityQueue mSendQueue;
-    Tasklet       mSendQueueTask;
+    SendQueueTask mSendQueueTask;
 
     Icmp mIcmp;
     Udp  mUdp;
@@ -376,6 +446,10 @@
 #if OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE
     MessageQueue mReassemblyList;
 #endif
+
+#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+    otBorderRoutingCounters mBorderRoutingCounters;
+#endif
 };
 
 /**
@@ -384,6 +458,8 @@
  */
 class Headers : private Clearable<Headers>
 {
+    friend class Clearable<Headers>;
+
 public:
     /**
      * This method parses the IPv6 and UDP/TCP/ICMP6 headers from a given message.
@@ -401,25 +477,20 @@
      *
      * @param[in]  aMessage         The message from which to read the lowpan frame.
      * @param[in]  aOffset          The offset in @p aMessage to start reading the frame.
-     * @param[in]  aMacSource       The MAC source address.
-     * @param[in]  aMacDest         The MAC destination address.
+     * @param[in]  aMacAddrs        The MAC source and destination addresses.
      *
      * @retval kErrorNone           Successfully decompressed and parsed IPv6 and UDP/TCP/ICMP6 headers.
      * @retval kErrorNotFound       Lowpan frame is a next fragment and does not contain IPv6 headers.
      * @retval kErrorParse          Failed to parse the headers.
      *
      */
-    Error DecompressFrom(const Message &     aMessage,
-                         uint16_t            aOffset,
-                         const Mac::Address &aMacSource,
-                         const Mac::Address &aMacDest);
+    Error DecompressFrom(const Message &aMessage, uint16_t aOffset, const Mac::Addresses &aMacAddrs);
 
     /**
      * This method decompresses lowpan frame and parses the IPv6 and UDP/TCP/ICMP6 headers.
      *
      * @param[in]  aFrameData       The lowpan frame data.
-     * @param[in]  aMacSource       The MAC source address.
-     * @param[in]  aMacDest         The MAC destination address.
+     * @param[in]  aMacAddrs        The MAC source and destination addresses.
      * @param[in]  aInstance        The OpenThread instance.
      *
      * @retval kErrorNone           Successfully decompressed and parsed IPv6 and UDP/TCP/ICMP6 headers.
@@ -427,10 +498,7 @@
      * @retval kErrorParse          Failed to parse the headers.
      *
      */
-    Error DecompressFrom(const FrameData &   aFrameData,
-                         const Mac::Address &aMacSource,
-                         const Mac::Address &aMacDest,
-                         Instance &          aInstance);
+    Error DecompressFrom(const FrameData &aFrameData, const Mac::Addresses &aMacAddrs, Instance &aInstance);
 
     /**
      * This method returns the IPv6 header.
diff --git a/src/core/net/ip6_address.cpp b/src/core/net/ip6_address.cpp
index 7f50c94..58ac92d 100644
--- a/src/core/net/ip6_address.cpp
+++ b/src/core/net/ip6_address.cpp
@@ -40,6 +40,7 @@
 #include "common/code_utils.hpp"
 #include "common/encoding.hpp"
 #include "common/instance.hpp"
+#include "common/num_utils.hpp"
 #include "common/numeric_limits.hpp"
 #include "common/random.hpp"
 #include "net/ip4_types.hpp"
@@ -69,18 +70,44 @@
     mLength = aLength;
 }
 
+bool Prefix::IsLinkLocal(void) const
+{
+    return (mLength >= 10) && ((mPrefix.mFields.m16[0] & HostSwap16(0xffc0)) == HostSwap16(0xfe80));
+}
+
+bool Prefix::IsMulticast(void) const { return (mLength >= 8) && (mPrefix.mFields.m8[0] == 0xff); }
+
+bool Prefix::IsUniqueLocal(void) const { return (mLength >= 7) && ((mPrefix.mFields.m8[0] & 0xfe) == 0xfc); }
+
 bool Prefix::IsEqual(const uint8_t *aPrefixBytes, uint8_t aPrefixLength) const
 {
     return (mLength == aPrefixLength) && (MatchLength(GetBytes(), aPrefixBytes, GetBytesSize()) >= mLength);
 }
 
+bool Prefix::ContainsPrefix(const Prefix &aSubPrefix) const
+{
+    return (mLength >= aSubPrefix.mLength) &&
+           (MatchLength(GetBytes(), aSubPrefix.GetBytes(), aSubPrefix.GetBytesSize()) >= aSubPrefix.GetLength());
+}
+
+bool Prefix::ContainsPrefix(const NetworkPrefix &aSubPrefix) const
+{
+    return (mLength >= NetworkPrefix::kLength) &&
+           (MatchLength(GetBytes(), aSubPrefix.m8, NetworkPrefix::kSize) >= NetworkPrefix::kLength);
+}
+
+bool Prefix::operator==(const Prefix &aOther) const
+{
+    return (mLength == aOther.mLength) && (MatchLength(GetBytes(), aOther.GetBytes(), GetBytesSize()) >= GetLength());
+}
+
 bool Prefix::operator<(const Prefix &aOther) const
 {
     bool    isSmaller;
     uint8_t minLength;
     uint8_t matchedLength;
 
-    minLength     = OT_MIN(GetLength(), aOther.GetLength());
+    minLength     = Min(GetLength(), aOther.GetLength());
     matchedLength = MatchLength(GetBytes(), aOther.GetBytes(), SizeForLength(minLength));
 
     if (matchedLength >= minLength)
@@ -130,6 +157,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;
@@ -163,20 +215,14 @@
 //---------------------------------------------------------------------------------------------------------------------
 // InterfaceIdentifier methods
 
-bool InterfaceIdentifier::IsUnspecified(void) const
-{
-    return (mFields.m32[0] == 0) && (mFields.m32[1] == 0);
-}
+bool InterfaceIdentifier::IsUnspecified(void) const { return (mFields.m32[0] == 0) && (mFields.m32[1] == 0); }
 
 bool InterfaceIdentifier::IsReserved(void) const
 {
     return IsSubnetRouterAnycast() || IsReservedSubnetAnycast() || IsAnycastLocator();
 }
 
-bool InterfaceIdentifier::IsSubnetRouterAnycast(void) const
-{
-    return (mFields.m32[0] == 0) && (mFields.m32[1] == 0);
-}
+bool InterfaceIdentifier::IsSubnetRouterAnycast(void) const { return (mFields.m32[0] == 0) && (mFields.m32[1] == 0); }
 
 bool InterfaceIdentifier::IsReservedSubnetAnycast(void) const
 {
@@ -191,15 +237,9 @@
             mFields.m8[7] >= 0x80);
 }
 
-void InterfaceIdentifier::GenerateRandom(void)
-{
-    SuccessOrAssert(Random::Crypto::FillBuffer(mFields.m8, kSize));
-}
+void InterfaceIdentifier::GenerateRandom(void) { SuccessOrAssert(Random::Crypto::FillBuffer(mFields.m8, kSize)); }
 
-void InterfaceIdentifier::SetBytes(const uint8_t *aBuffer)
-{
-    memcpy(mFields.m8, aBuffer, kSize);
-}
+void InterfaceIdentifier::SetBytes(const uint8_t *aBuffer) { memcpy(mFields.m8, aBuffer, kSize); }
 
 void InterfaceIdentifier::SetFromExtAddress(const Mac::ExtAddress &aExtAddress)
 {
@@ -254,6 +294,15 @@
     return (IsLocator() && (locator >= Mle::kAloc16ServiceStart) && (locator <= Mle::kAloc16ServiceEnd));
 }
 
+void InterfaceIdentifier::ApplyPrefix(const Prefix &aPrefix)
+{
+    if (aPrefix.GetLength() > NetworkPrefix::kLength)
+    {
+        Address::CopyBits(mFields.m8, aPrefix.GetBytes() + NetworkPrefix::kSize,
+                          aPrefix.GetLength() - NetworkPrefix::kLength);
+    }
+}
+
 InterfaceIdentifier::InfoString InterfaceIdentifier::ToString(void) const
 {
     InfoString string;
@@ -276,10 +325,7 @@
     return (mFields.m32[0] == 0 && mFields.m32[1] == 0 && mFields.m32[2] == 0 && mFields.m32[3] == HostSwap32(1));
 }
 
-bool Address::IsLinkLocal(void) const
-{
-    return (mFields.m16[0] & HostSwap16(0xffc0)) == HostSwap16(0xfe80);
-}
+bool Address::IsLinkLocal(void) const { return (mFields.m16[0] & HostSwap16(0xffc0)) == HostSwap16(0xfe80); }
 
 void Address::SetToLinkLocalAddress(const Mac::ExtAddress &aExtAddress)
 {
@@ -295,70 +341,31 @@
     SetIid(aIid);
 }
 
-bool Address::IsLinkLocalMulticast(void) const
-{
-    return IsMulticast() && (GetScope() == kLinkLocalScope);
-}
+bool Address::IsLinkLocalMulticast(void) const { return IsMulticast() && (GetScope() == kLinkLocalScope); }
 
-bool Address::IsLinkLocalAllNodesMulticast(void) const
-{
-    return (*this == GetLinkLocalAllNodesMulticast());
-}
+bool Address::IsLinkLocalAllNodesMulticast(void) const { return (*this == GetLinkLocalAllNodesMulticast()); }
 
-void Address::SetToLinkLocalAllNodesMulticast(void)
-{
-    *this = GetLinkLocalAllNodesMulticast();
-}
+void Address::SetToLinkLocalAllNodesMulticast(void) { *this = GetLinkLocalAllNodesMulticast(); }
 
-bool Address::IsLinkLocalAllRoutersMulticast(void) const
-{
-    return (*this == GetLinkLocalAllRoutersMulticast());
-}
+bool Address::IsLinkLocalAllRoutersMulticast(void) const { return (*this == GetLinkLocalAllRoutersMulticast()); }
 
-void Address::SetToLinkLocalAllRoutersMulticast(void)
-{
-    *this = GetLinkLocalAllRoutersMulticast();
-}
+void Address::SetToLinkLocalAllRoutersMulticast(void) { *this = GetLinkLocalAllRoutersMulticast(); }
 
-bool Address::IsRealmLocalMulticast(void) const
-{
-    return IsMulticast() && (GetScope() == kRealmLocalScope);
-}
+bool Address::IsRealmLocalMulticast(void) const { return IsMulticast() && (GetScope() == kRealmLocalScope); }
 
-bool Address::IsMulticastLargerThanRealmLocal(void) const
-{
-    return IsMulticast() && (GetScope() > kRealmLocalScope);
-}
+bool Address::IsMulticastLargerThanRealmLocal(void) const { return IsMulticast() && (GetScope() > kRealmLocalScope); }
 
-bool Address::IsRealmLocalAllNodesMulticast(void) const
-{
-    return (*this == GetRealmLocalAllNodesMulticast());
-}
+bool Address::IsRealmLocalAllNodesMulticast(void) const { return (*this == GetRealmLocalAllNodesMulticast()); }
 
-void Address::SetToRealmLocalAllNodesMulticast(void)
-{
-    *this = GetRealmLocalAllNodesMulticast();
-}
+void Address::SetToRealmLocalAllNodesMulticast(void) { *this = GetRealmLocalAllNodesMulticast(); }
 
-bool Address::IsRealmLocalAllRoutersMulticast(void) const
-{
-    return (*this == GetRealmLocalAllRoutersMulticast());
-}
+bool Address::IsRealmLocalAllRoutersMulticast(void) const { return (*this == GetRealmLocalAllRoutersMulticast()); }
 
-void Address::SetToRealmLocalAllRoutersMulticast(void)
-{
-    *this = GetRealmLocalAllRoutersMulticast();
-}
+void Address::SetToRealmLocalAllRoutersMulticast(void) { *this = GetRealmLocalAllRoutersMulticast(); }
 
-bool Address::IsRealmLocalAllMplForwarders(void) const
-{
-    return (*this == GetRealmLocalAllMplForwarders());
-}
+bool Address::IsRealmLocalAllMplForwarders(void) const { return (*this == GetRealmLocalAllMplForwarders()); }
 
-void Address::SetToRealmLocalAllMplForwarders(void)
-{
-    *this = GetRealmLocalAllMplForwarders();
-}
+void Address::SetToRealmLocalAllMplForwarders(void) { *this = GetRealmLocalAllMplForwarders(); }
 
 bool Address::MatchesPrefix(const Prefix &aPrefix) const
 {
@@ -370,42 +377,37 @@
     return Prefix::MatchLength(mFields.m8, aPrefix, Prefix::SizeForLength(aPrefixLength)) >= aPrefixLength;
 }
 
-void Address::SetPrefix(const NetworkPrefix &aNetworkPrefix)
+void Address::SetPrefix(const NetworkPrefix &aNetworkPrefix) { mFields.mComponents.mNetworkPrefix = aNetworkPrefix; }
+
+void Address::SetPrefix(const Prefix &aPrefix) { CopyBits(mFields.m8, aPrefix.GetBytes(), aPrefix.GetLength()); }
+
+void Address::CopyBits(uint8_t *aDst, const uint8_t *aSrc, uint8_t aNumBits)
 {
-    mFields.mComponents.mNetworkPrefix = aNetworkPrefix;
-}
+    // This method copies `aNumBits` from `aSrc` into `aDst` handling
+    // the case where `aNumBits` may not be a multiple of 8. It leaves the
+    // remaining bits beyond `aNumBits` in `aDst` unchanged.
 
-void Address::SetPrefix(const Prefix &aPrefix)
-{
-    SetPrefix(0, aPrefix.GetBytes(), aPrefix.GetLength());
-}
+    uint8_t numBytes  = aNumBits / CHAR_BIT;
+    uint8_t extraBits = aNumBits % CHAR_BIT;
 
-void Address::SetPrefix(uint8_t aOffset, const uint8_t *aPrefix, uint8_t aPrefixLength)
-{
-    uint8_t bytes     = aPrefixLength / CHAR_BIT;
-    uint8_t extraBits = aPrefixLength % CHAR_BIT;
-
-    OT_ASSERT(aPrefixLength <= (sizeof(Address) - aOffset) * CHAR_BIT);
-
-    memcpy(mFields.m8 + aOffset, aPrefix, bytes);
+    memcpy(aDst, aSrc, numBytes);
 
     if (extraBits > 0)
     {
-        uint8_t index = aOffset + bytes;
-        uint8_t mask  = ((0x80 >> (extraBits - 1)) - 1);
+        uint8_t mask = ((0x80 >> (extraBits - 1)) - 1);
 
         // `mask` has its higher (msb) `extraBits` bits as `0` and the remaining as `1`.
         // Example with `extraBits` = 3:
         // ((0x80 >> 2) - 1) = (0b0010_0000 - 1) = 0b0001_1111
 
-        mFields.m8[index] &= mask;
-        mFields.m8[index] |= (aPrefix[index] & ~mask);
+        aDst[numBytes] &= mask;
+        aDst[numBytes] |= (aSrc[numBytes] & ~mask);
     }
 }
 
 void Address::SetMulticastNetworkPrefix(const uint8_t *aPrefix, uint8_t aPrefixLength)
 {
-    SetPrefix(kMulticastNetworkPrefixOffset, aPrefix, aPrefixLength);
+    CopyBits(&mFields.m8[kMulticastNetworkPrefixOffset], aPrefix, aPrefixLength);
     mFields.m8[kMulticastNetworkPrefixLengthOffset] = aPrefixLength;
 }
 
@@ -473,7 +475,7 @@
 {
     // The prefix length must be 32, 40, 48, 56, 64, 96. IPv4 bytes are added
     // after the prefix, skipping over the bits 64 to 71 (byte at `kSkipIndex`)
-    // which must be set to zero. The suffix is set to zero (per RFC 6502).
+    // which must be set to zero. The suffix is set to zero (per RFC 6052).
     //
     //    +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
     //    |PL| 0-------------32--40--48--56--64--72--80--88--96--104---------|
@@ -515,10 +517,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;
@@ -536,7 +544,7 @@
         colonIndex = index;
     }
 
-    while (*aString != kNullChar)
+    while (*aString != aTerminatorChar)
     {
         const char *start = aString;
         uint32_t    value = 0;
@@ -583,7 +591,7 @@
             break;
         }
 
-        VerifyOrExit((*aString == kColonChar) || (*aString == kNullChar));
+        VerifyOrExit((*aString == kColonChar) || (*aString == aTerminatorChar));
 
         VerifyOrExit(index < endIndex);
         mFields.m16[index++] = HostSwap16(static_cast<uint16_t>(value));
@@ -617,7 +625,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 064e282..3323690 100644
--- a/src/core/net/ip6_address.hpp
+++ b/src/core/net/ip6_address.hpp
@@ -188,10 +188,7 @@
      * @retval FALSE  The prefix is not a Link-Local prefix.
      *
      */
-    bool IsLinkLocal(void) const
-    {
-        return mLength >= 10 && mPrefix.mFields.m8[0] == 0xfe && (mPrefix.mFields.m8[1] & 0xc0) == 0x80;
-    }
+    bool IsLinkLocal(void) const;
 
     /**
      * This method indicates whether the prefix is a Multicast prefix.
@@ -200,7 +197,7 @@
      * @retval FALSE  The prefix is not a Multicast prefix.
      *
      */
-    bool IsMulticast(void) const { return mLength >= 8 && mPrefix.mFields.m8[0] == 0xff; }
+    bool IsMulticast(void) const;
 
     /**
      * This method indicates whether the prefix is a Unique-Local prefix.
@@ -209,7 +206,7 @@
      * @retval FALSE  The prefix is not a Unique-Local prefix.
      *
      */
-    bool IsUniqueLocal(void) const { return mLength >= 7 && (mPrefix.mFields.m8[0] & 0xfe) == 0xfc; }
+    bool IsUniqueLocal(void) const;
 
     /**
      * This method indicates whether the prefix is equal to a given prefix.
@@ -232,11 +229,7 @@
      * @retval FALSE  The prefix does not contains the @p aSubPrefix.
      *
      */
-    bool ContainsPrefix(const Prefix &aSubPrefix) const
-    {
-        return (mLength >= aSubPrefix.mLength) &&
-               (MatchLength(GetBytes(), aSubPrefix.GetBytes(), aSubPrefix.GetBytesSize()) >= aSubPrefix.GetLength());
-    }
+    bool ContainsPrefix(const Prefix &aSubPrefix) const;
 
     /**
      * This method indicates whether the prefix contains a sub-prefix (given as a `NetworkPrefix`).
@@ -247,11 +240,7 @@
      * @retval FALSE  The prefix does not contains the @p aSubPrefix.
      *
      */
-    bool ContainsPrefix(const NetworkPrefix &aSubPrefix) const
-    {
-        return (mLength >= NetworkPrefix::kLength) &&
-               (MatchLength(GetBytes(), aSubPrefix.m8, NetworkPrefix::kSize) >= NetworkPrefix::kLength);
-    }
+    bool ContainsPrefix(const NetworkPrefix &aSubPrefix) const;
 
     /**
      * This method overloads operator `==` to evaluate whether or not two prefixes are equal.
@@ -262,11 +251,7 @@
      * @retval FALSE  If the two prefixes are not equal.
      *
      */
-    bool operator==(const Prefix &aOther) const
-    {
-        return (mLength == aOther.mLength) &&
-               (MatchLength(GetBytes(), aOther.GetBytes(), GetBytesSize()) >= GetLength());
-    }
+    bool operator==(const Prefix &aOther) const;
 
     /**
      * This method overloads operator `<` to compare two prefixes.
@@ -309,7 +294,7 @@
     /**
      * This method indicates whether or not a given prefix length is valid for use as a NAT64 prefix.
      *
-     * A NAT64 prefix must have one of the following lengths: 32, 40, 48, 56, 64, or 96 (per RFC 6502).
+     * A NAT64 prefix must have one of the following lengths: 32, 40, 48, 56, 64, or 96 (per RFC 6052).
      *
      * @param[in] aLength The length of the prefix.
      *
@@ -322,7 +307,7 @@
     /**
      * This method indicates whether or not the prefix has a valid length for use as a NAT64 prefix.
      *
-     * A NAT64 prefix must have one of the following lengths: 32, 40, 48, 56, 64, or 96 (per RFC 6502).
+     * A NAT64 prefix must have one of the following lengths: 32, 40, 48, 56, 64, or 96 (per RFC 6052).
      *
      * @retval TRUE   If the prefix has a valid length for use as a NAT64 prefix.
      * @retval FALSE  If the prefix does not have a valid length for use as a NAT64 prefix.
@@ -331,6 +316,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".
@@ -540,6 +536,17 @@
     void SetLocator(uint16_t aLocator) { mFields.m16[3] = HostSwap16(aLocator); }
 
     /**
+     * This method applies a prefix to IID.
+     *
+     * If the prefix length is longer than 64 bits, the prefix bits after 64 are written into the IID. This method only
+     * changes the bits in IID up the prefix length and keeps the rest of the bits in IID as before.
+     *
+     * @param[in] aPrefix   An IPv6 prefix.
+     *
+     */
+    void ApplyPrefix(const Prefix &aPrefix);
+
+    /**
      * This method converts an Interface Identifier to a string.
      *
      * @returns An `InfoString` containing the string representation of the Interface Identifier.
@@ -561,6 +568,7 @@
 class Address : public otIp6Address, public Equatable<Address>, public Clearable<Address>
 {
     friend class Prefix;
+    friend class InterfaceIdentifier;
 
 public:
     static constexpr uint8_t kAloc16Mask = InterfaceIdentifier::kAloc16Mask; ///< The mask for ALOC16.
@@ -807,6 +815,15 @@
     }
 
     /**
+     * This method gets a prefix of the IPv6 address with a given length.
+     *
+     * @param[in]  aLength  The length of prefix in bits.
+     * @param[out] aPrefix  A reference to a prefix to output the fetched prefix.
+     *
+     */
+    void GetPrefix(uint8_t aLength, Prefix &aPrefix) const { aPrefix.Set(mFields.m8, aLength); }
+
+    /**
      * This method indicates whether the IPv6 address matches a given prefix.
      *
      * @param[in] aPrefix  An IPv6 prefix to match with.
@@ -839,7 +856,7 @@
      * @param[in]  aPrefixLength   The prefix length (in bits).
      *
      */
-    void SetPrefix(const uint8_t *aPrefix, uint8_t aPrefixLength) { SetPrefix(0, aPrefix, aPrefixLength); }
+    void SetPrefix(const uint8_t *aPrefix, uint8_t aPrefixLength) { CopyBits(mFields.m8, aPrefix, aPrefixLength); }
 
     /**
      * This method sets the IPv6 address prefix to the given Network Prefix.
@@ -1007,7 +1024,9 @@
     bool operator<(const Address &aOther) const { return memcmp(mFields.m8, aOther.mFields.m8, sizeof(Address)) < 0; }
 
 private:
-    void SetPrefix(uint8_t aOffset, const uint8_t *aPrefix, uint8_t aPrefixLength);
+    static constexpr uint8_t kMulticastNetworkPrefixLengthOffset = 3; // Prefix-Based Multicast Address (RFC3306)
+    static constexpr uint8_t kMulticastNetworkPrefixOffset       = 4; // Prefix-Based Multicast Address (RFC3306)
+
     void SetToLocator(const NetworkPrefix &aNetworkPrefix, uint16_t aLocator);
     void ToString(StringWriter &aWriter) const;
     void AppendHexWords(StringWriter &aWriter, uint8_t aLength) const;
@@ -1018,8 +1037,10 @@
     static const Address &GetRealmLocalAllRoutersMulticast(void);
     static const Address &GetRealmLocalAllMplForwarders(void);
 
-    static constexpr uint8_t kMulticastNetworkPrefixLengthOffset = 3; // Prefix-Based Multicast Address (RFC3306).
-    static constexpr uint8_t kMulticastNetworkPrefixOffset       = 4; // Prefix-Based Multicast Address (RFC3306).
+    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_headers.cpp b/src/core/net/ip6_headers.cpp
index be72fb6..ff9585f 100644
--- a/src/core/net/ip6_headers.cpp
+++ b/src/core/net/ip6_headers.cpp
@@ -38,6 +38,9 @@
 namespace ot {
 namespace Ip6 {
 
+//---------------------------------------------------------------------------------------------------------------------
+// Header
+
 Error Header::ParseFrom(const Message &aMessage)
 {
     Error error = kErrorParse;
@@ -63,5 +66,68 @@
     return IsVersion6() && ((sizeof(Header) + GetPayloadLength()) <= kMaxLength);
 }
 
+//---------------------------------------------------------------------------------------------------------------------
+// Option
+
+Error Option::ParseFrom(const Message &aMessage, uint16_t aOffset, uint16_t aEndOffset)
+{
+    Error error;
+
+    // Read the Type first to check for the Pad1 Option.
+    // If it is not, then we read the full `Option` header.
+
+    SuccessOrExit(error = aMessage.Read(aOffset, this, sizeof(mType)));
+
+    if (mType == kTypePad1)
+    {
+        SetLength(0);
+        ExitNow();
+    }
+
+    SuccessOrExit(error = aMessage.Read(aOffset, *this));
+
+    VerifyOrExit(aOffset + GetSize() <= aEndOffset, error = kErrorParse);
+
+exit:
+    return error;
+}
+
+uint16_t Option::GetSize(void) const
+{
+    return (mType == kTypePad1) ? sizeof(mType) : static_cast<uint16_t>(mLength) + sizeof(Option);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// PadOption
+
+void PadOption::InitForPadSize(uint8_t aPadSize)
+{
+    OT_UNUSED_VARIABLE(mPads);
+
+    Clear();
+
+    if (aPadSize == 1)
+    {
+        SetType(kTypePad1);
+    }
+    else
+    {
+        SetType(kTypePadN);
+        SetLength(aPadSize - sizeof(Option));
+    }
+}
+
+Error PadOption::InitToPadHeaderWithSize(uint16_t aHeaderSize)
+{
+    Error   error = kErrorNone;
+    uint8_t size  = static_cast<uint8_t>(aHeaderSize % ExtensionHeader::kLengthUnitSize);
+
+    VerifyOrExit(size != 0, error = kErrorAlready);
+    InitForPadSize(ExtensionHeader::kLengthUnitSize - size);
+
+exit:
+    return error;
+}
+
 } // namespace Ip6
 } // namespace ot
diff --git a/src/core/net/ip6_headers.hpp b/src/core/net/ip6_headers.hpp
index c6846a7..bb2ba10 100644
--- a/src/core/net/ip6_headers.hpp
+++ b/src/core/net/ip6_headers.hpp
@@ -378,6 +378,14 @@
 {
 public:
     /**
+     * This constant defines the size of Length unit in bytes.
+     *
+     * The Length field is in 8-bytes unit. The total size of `ExtensionHeader` MUST be a multiple of 8.
+     *
+     */
+    static constexpr uint16_t kLengthUnitSize = 8;
+
+    /**
      * This method returns the IPv6 Next Header value.
      *
      * @returns The IPv6 Next Header value.
@@ -396,6 +404,8 @@
     /**
      * This method returns the IPv6 Header Extension Length value.
      *
+     * The Length is in 8-byte units and does not include the first 8 bytes.
+     *
      * @returns The IPv6 Header Extension Length value.
      *
      */
@@ -404,12 +414,27 @@
     /**
      * This method sets the IPv6 Header Extension Length value.
      *
+     * The Length is in 8-byte units and does not include the first 8 bytes.
+     *
      * @param[in]  aLength  The IPv6 Header Extension Length value.
      *
      */
     void SetLength(uint8_t aLength) { mLength = aLength; }
 
+    /**
+     * This method returns the size (number of bytes) of the Extension Header including Next Header and Length fields.
+     *
+     * @returns The size (number of bytes) of the Extension Header.
+     *
+     */
+    uint16_t GetSize(void) const { return kLengthUnitSize * (mLength + 1); }
+
 private:
+    // |     m8[0]     |     m8[1]     |     m8[2]     |      m8[3]    |
+    // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    // | Next Header   | Header Length | . . .                         |
+    // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
     uint8_t mNextHeader;
     uint8_t mLength;
 } OT_TOOL_PACKED_END;
@@ -428,18 +453,20 @@
  *
  */
 OT_TOOL_PACKED_BEGIN
-class OptionHeader
+class Option
 {
 public:
     /**
-     * Default constructor.
+     * IPv6 Option Type actions for unrecognized IPv6 Options.
      *
      */
-    OptionHeader(void)
-        : mType(0)
-        , mLength(0)
+    enum Action : uint8_t
     {
-    }
+        kActionSkip      = 0x00, ///< Skip over this option and continue processing the header.
+        kActionDiscard   = 0x40, ///< Discard the packet.
+        kActionForceIcmp = 0x80, ///< Discard the packet and forcibly send an ICMP Parameter Problem.
+        kActionIcmp      = 0xc0, ///< Discard packet and conditionally send an ICMP Parameter Problem.
+    };
 
     /**
      * This method returns the IPv6 Option Type value.
@@ -450,24 +477,13 @@
     uint8_t GetType(void) const { return mType; }
 
     /**
-     * This method sets the IPv6 Option Type value.
+     * This method indicates whether IPv6 Option is padding (either Pad1 or PadN).
      *
-     * @param[in]  aType  The IPv6 Option Type value.
+     * @retval TRUE   The Option is padding.
+     * @retval FALSE  The Option is not padding.
      *
      */
-    void SetType(uint8_t aType) { mType = aType; }
-
-    /**
-     * IPv6 Option Type actions for unrecognized IPv6 Options.
-     *
-     */
-    enum Action : uint8_t
-    {
-        kActionSkip      = 0x00, ///< skip over this option and continue processing the header
-        kActionDiscard   = 0x40, ///< discard the packet
-        kActionForceIcmp = 0x80, ///< discard the packet and forcibly send an ICMP Parameter Problem
-        kActionIcmp      = 0xc0, ///< discard packet and conditionally send an ICMP Parameter Problem
-    };
+    bool IsPadding(void) const { return (mType == kTypePad1) || (mType == kTypePadN); }
 
     /**
      * This method returns the IPv6 Option action for unrecognized IPv6 Options.
@@ -486,6 +502,46 @@
     uint8_t GetLength(void) const { return mLength; }
 
     /**
+     * This method returns the size (number of bytes) of the IPv6 Option.
+     *
+     * This method returns the proper size of the Option independent of its type, particularly if Option is Pad1 (which
+     * does not follow the common Option header structure and has only Type field with no Length field). For other
+     * Option types, the returned size includes the Type and Length fields.
+     *
+     * @returns The size of the Option.
+     *
+     */
+    uint16_t GetSize(void) const;
+
+    /**
+     * This method parses and validates the IPv6 Option from a given message.
+     *
+     * The Option is read from @p aOffset in @p aMessage. This method then checks that the entire Option is present
+     * in @p aMessage before the @p aEndOffset.
+     *
+     * @param[in]  aMessage    The IPv6 message.
+     * @param[in]  aOffset     The offset in @p aMessage to read the IPv6 Option.
+     * @param[in]  aEndOffset  The end offset in @p aMessage.
+     *
+     * @retval kErrorNone   Successfully parsed the IPv6 option from @p aMessage.
+     * @retval kErrorParse  Malformed IPv6 Option or Option is not contained within @p aMessage by @p aEndOffset.
+     *
+     */
+    Error ParseFrom(const Message &aMessage, uint16_t aOffset, uint16_t aEndOffset);
+
+protected:
+    static constexpr uint8_t kTypePad1 = 0x00; ///< Pad1 Option Type.
+    static constexpr uint8_t kTypePadN = 0x01; ///< PanN Option Type.
+
+    /**
+     * This method sets the IPv6 Option Type value.
+     *
+     * @param[in]  aType  The IPv6 Option Type value.
+     *
+     */
+    void SetType(uint8_t aType) { mType = aType; }
+
+    /**
      * This method sets the IPv6 Option Length value.
      *
      * @param[in]  aLength  The IPv6 Option Length value.
@@ -501,62 +557,45 @@
 } OT_TOOL_PACKED_END;
 
 /**
- * This class implements IPv6 PadN Option generation and parsing.
+ * This class implements IPv6 Pad Options (Pad1 or PadN) generation.
  *
  */
 OT_TOOL_PACKED_BEGIN
-class OptionPadN : public OptionHeader
+class PadOption : public Option, private Clearable<PadOption>
 {
+    friend class Clearable<PadOption>;
+
 public:
-    static constexpr uint8_t kType      = 0x01; ///< PadN type
-    static constexpr uint8_t kData      = 0x00; ///< PadN specific data
-    static constexpr uint8_t kMaxLength = 0x05; ///< Maximum length of PadN option data
-
     /**
-     * This method initializes the PadN header.
+     * This method initializes the Pad Option for a given total Pad size.
      *
-     * @param[in]  aPadLength  The length of needed padding. Allowed value from
-     *                         range 2-7.
+     * The @p aPadSize MUST be from range 1-7. Otherwise the behavior of this method is undefined.
+     *
+     * @param[in]  aPadSize  The total number of needed padding bytes.
      *
      */
-    void Init(uint8_t aPadLength)
-    {
-        SetType(kType);
-        SetLength(aPadLength - sizeof(OptionHeader));
-        memset(mPad, kData, aPadLength - sizeof(OptionHeader));
-    }
+    void InitForPadSize(uint8_t aPadSize);
 
     /**
-     * This method returns the total IPv6 Option Length value including option
-     * header.
+     * This method initializes the Pad Option for padding an IPv6 Extension header with a given current size.
      *
-     * @returns The total IPv6 Option Length.
+     * The Extension Header Length is in 8-bytes unit, so the total size should be a multiple of 8. This method
+     * determines the Pad Option size needed for appending to Extension Header based on it current size @p aHeaderSize
+     * so to make it a multiple of 8. This method returns `kErrorAlready` when the @p aHeaderSize is already
+     * a multiple of 8 (i.e., no padding is needed).
+     *
+     * @param[in] aHeaderSize  The current IPv6 Extension header size (in bytes).
+     *
+     * @retval kErrorNone     The Pad Option is successfully initialized.
+     * @retval kErrorAlready  The @p aHeaderSize is already a multiple of 8 and no padding is needed.
      *
      */
-    uint8_t GetTotalLength(void) const { return GetLength() + sizeof(OptionHeader); }
+    Error InitToPadHeaderWithSize(uint16_t aHeaderSize);
 
 private:
-    uint8_t mPad[kMaxLength];
-} OT_TOOL_PACKED_END;
+    static constexpr uint8_t kMaxLength = 5;
 
-/**
- * This class implements IPv6 Pad1 Option generation and parsing. Pad1 does not follow default option header structure.
- *
- */
-OT_TOOL_PACKED_BEGIN
-class OptionPad1
-{
-public:
-    static constexpr uint8_t kType = 0x00;
-
-    /**
-     * This method initializes the Pad1 header.
-     *
-     */
-    void Init(void) { mType = kType; }
-
-private:
-    uint8_t mType;
+    uint8_t mPads[kMaxLength];
 } OT_TOOL_PACKED_END;
 
 /**
diff --git a/src/core/net/ip6_mpl.cpp b/src/core/net/ip6_mpl.cpp
index 3e0156a..f39d833 100644
--- a/src/core/net/ip6_mpl.cpp
+++ b/src/core/net/ip6_mpl.cpp
@@ -34,6 +34,7 @@
 #include "ip6_mpl.hpp"
 
 #include "common/code_utils.hpp"
+#include "common/debug.hpp"
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
 #include "common/message.hpp"
@@ -46,52 +47,73 @@
 
 Mpl::Mpl(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mMatchingAddress(nullptr)
-    , mSeedSetTimer(aInstance, Mpl::HandleSeedSetTimer)
-    , mSeedId(0)
     , mSequence(0)
 #if OPENTHREAD_FTD
-    , mRetransmissionTimer(aInstance, Mpl::HandleRetransmissionTimer)
-    , mTimerExpirations(0)
+    , mRetransmissionTimer(aInstance)
 #endif
 {
     memset(mSeedSet, 0, sizeof(mSeedSet));
 }
 
-void Mpl::InitOption(OptionMpl &aOption, const Address &aAddress)
+void MplOption::Init(SeedIdLength aSeedIdLength)
 {
-    aOption.Init();
-    aOption.SetSequence(mSequence++);
+    SetType(kType);
 
-    // Check if Seed Id can be elided.
-    if (mMatchingAddress && aAddress == *mMatchingAddress)
+    switch (aSeedIdLength)
     {
-        aOption.SetSeedIdLength(OptionMpl::kSeedIdLength0);
+    case kSeedIdLength0:
+        SetLength(sizeof(*this) - sizeof(Option) - sizeof(mSeedId));
+        break;
+    case kSeedIdLength2:
+        SetLength(sizeof(*this) - sizeof(Option));
+        break;
+    default:
+        OT_ASSERT(false);
+    }
 
-        // Decrease default option length.
-        aOption.SetLength(aOption.GetLength() - sizeof(mSeedId));
+    mControl = aSeedIdLength;
+}
+
+void Mpl::InitOption(MplOption &aOption, const Address &aAddress)
+{
+    if (aAddress == Get<Mle::Mle>().GetMeshLocal16())
+    {
+        // Seed ID can be elided when `aAddress` is RLOC.
+        aOption.Init(MplOption::kSeedIdLength0);
     }
     else
     {
-        aOption.SetSeedIdLength(OptionMpl::kSeedIdLength2);
-        aOption.SetSeedId(mSeedId);
+        aOption.Init(MplOption::kSeedIdLength2);
+        aOption.SetSeedId(Get<Mle::Mle>().GetRloc16());
     }
+
+    aOption.SetSequence(mSequence++);
 }
 
-Error Mpl::ProcessOption(Message &aMessage, const Address &aAddress, bool aIsOutbound, bool &aReceive)
+Error Mpl::ProcessOption(Message &aMessage, uint16_t aOffset, const Address &aAddress, bool aIsOutbound, bool &aReceive)
 {
     Error     error;
-    OptionMpl option;
+    MplOption option;
 
-    VerifyOrExit(aMessage.ReadBytes(aMessage.GetOffset(), &option, sizeof(option)) >= OptionMpl::kMinLength &&
-                     (option.GetSeedIdLength() == OptionMpl::kSeedIdLength0 ||
-                      option.GetSeedIdLength() == OptionMpl::kSeedIdLength2),
-                 error = kErrorParse);
+    // Read the min size bytes first, then check the expected
+    // `SeedIdLength` and read the full `MplOption` if needed.
+    SuccessOrExit(error = aMessage.Read(aOffset, &option, MplOption::kMinSize));
 
-    if (option.GetSeedIdLength() == OptionMpl::kSeedIdLength0)
+    switch (option.GetSeedIdLength())
     {
-        // Retrieve MPL Seed Id from the IPv6 Source Address.
-        option.SetSeedId(HostSwap16(aAddress.mFields.m16[7]));
+    case MplOption::kSeedIdLength0:
+        // Retrieve Seed ID from the IPv6 Source Address RLOC.
+        VerifyOrExit(aAddress.GetIid().IsLocator(), error = kErrorDrop);
+        option.SetSeedId(aAddress.GetIid().GetLocator());
+        break;
+
+    case MplOption::kSeedIdLength2:
+        SuccessOrExit(error = aMessage.Read(aOffset, option));
+        break;
+
+    case MplOption::kSeedIdLength8:
+    case MplOption::kSeedIdLength16:
+        ExitNow(error = kErrorParse);
     }
 
     // Check if the MPL Data Message is new.
@@ -191,6 +213,8 @@
             if (aSequence == mSeedSet[i].mSequence)
             {
                 // already received, drop message
+
+                mSeedSet[i].mLifetime = kSeedEntryLifetime;
                 ExitNow(error = kErrorDrop);
             }
             else if (insert == nullptr && SerialNumber::IsLess(aSequence, mSeedSet[i].mSequence))
@@ -252,24 +276,16 @@
     insert->mSequence = aSequence;
     insert->mLifetime = kSeedEntryLifetime;
 
-    if (!mSeedSetTimer.IsRunning())
-    {
-        mSeedSetTimer.Start(kSeedEntryLifetimeDt);
-    }
+    Get<TimeTicker>().RegisterReceiver(TimeTicker::kIp6Mpl);
 
 exit:
     return error;
 }
 
-void Mpl::HandleSeedSetTimer(Timer &aTimer)
+void Mpl::HandleTimeTick(void)
 {
-    aTimer.Get<Mpl>().HandleSeedSetTimer();
-}
-
-void Mpl::HandleSeedSetTimer(void)
-{
-    bool startTimer = false;
-    int  j          = 0;
+    bool continueRxingTicks = false;
+    int  j                  = 0;
 
     for (int i = 0; i < kNumSeedEntries && mSeedSet[i].mLifetime; i++)
     {
@@ -277,8 +293,8 @@
 
         if (mSeedSet[i].mLifetime > 0)
         {
-            mSeedSet[j++] = mSeedSet[i];
-            startTimer    = true;
+            mSeedSet[j++]      = mSeedSet[i];
+            continueRxingTicks = true;
         }
     }
 
@@ -287,14 +303,37 @@
         mSeedSet[j].mLifetime = 0;
     }
 
-    if (startTimer)
+    if (!continueRxingTicks)
     {
-        mSeedSetTimer.Start(kSeedEntryLifetimeDt);
+        Get<TimeTicker>().UnregisterReceiver(TimeTicker::kIp6Mpl);
     }
 }
 
 #if OPENTHREAD_FTD
 
+uint8_t Mpl::GetTimerExpirations(void) const
+{
+    uint8_t timerExpirations = 0;
+
+    switch (Get<Mle::Mle>().GetRole())
+    {
+    case Mle::kRoleDisabled:
+    case Mle::kRoleDetached:
+        break;
+
+    case Mle::kRoleChild:
+        timerExpirations = kChildTimerExpirations;
+        break;
+
+    case Mle::kRoleRouter:
+    case Mle::kRoleLeader:
+        timerExpirations = kRouterTimerExpirations;
+        break;
+    }
+
+    return timerExpirations;
+}
+
 void Mpl::AddBufferedMessage(Message &aMessage, uint16_t aSeedId, uint8_t aSequence, bool aIsOutbound)
 {
     Error    error       = kErrorNone;
@@ -334,11 +373,6 @@
     FreeMessageOnError(messageCopy, error);
 }
 
-void Mpl::HandleRetransmissionTimer(Timer &aTimer)
-{
-    aTimer.Get<Mpl>().HandleRetransmissionTimer();
-}
-
 void Mpl::HandleRetransmissionTimer(void)
 {
     TimeMilli now      = TimerMilli::GetNow();
@@ -351,17 +385,16 @@
 
         if (now < metadata.mTransmissionTime)
         {
-            if (nextTime > metadata.mTransmissionTime)
-            {
-                nextTime = metadata.mTransmissionTime;
-            }
+            nextTime = Min(nextTime, metadata.mTransmissionTime);
         }
         else
         {
+            uint8_t timerExpirations = GetTimerExpirations();
+
             // Update the number of transmission timer expirations.
             metadata.mTransmissionCount++;
 
-            if (metadata.mTransmissionCount < GetTimerExpirations())
+            if (metadata.mTransmissionCount < timerExpirations)
             {
                 Message *messageCopy = message.Clone(message.GetLength() - sizeof(Metadata));
 
@@ -378,16 +411,13 @@
                 metadata.GenerateNextTransmissionTime(now, kDataMessageInterval);
                 metadata.UpdateIn(message);
 
-                if (nextTime > metadata.mTransmissionTime)
-                {
-                    nextTime = metadata.mTransmissionTime;
-                }
+                nextTime = Min(nextTime, metadata.mTransmissionTime);
             }
             else
             {
                 mBufferedMessageSet.Dequeue(message);
 
-                if (metadata.mTransmissionCount == GetTimerExpirations())
+                if (metadata.mTransmissionCount == timerExpirations)
                 {
                     if (metadata.mTransmissionCount > 1)
                     {
@@ -425,10 +455,7 @@
     SuccessOrAssert(aMessage.SetLength(aMessage.GetLength() - sizeof(*this)));
 }
 
-void Mpl::Metadata::UpdateIn(Message &aMessage) const
-{
-    aMessage.Write(aMessage.GetLength() - sizeof(*this), *this);
-}
+void Mpl::Metadata::UpdateIn(Message &aMessage) const { aMessage.Write(aMessage.GetLength() - sizeof(*this), *this); }
 
 void Mpl::Metadata::GenerateNextTransmissionTime(TimeMilli aCurrentTime, uint8_t aInterval)
 {
diff --git a/src/core/net/ip6_mpl.hpp b/src/core/net/ip6_mpl.hpp
index d6cdfac..60fd668 100644
--- a/src/core/net/ip6_mpl.hpp
+++ b/src/core/net/ip6_mpl.hpp
@@ -39,6 +39,7 @@
 #include "common/locator.hpp"
 #include "common/message.hpp"
 #include "common/non_copyable.hpp"
+#include "common/time_ticker.hpp"
 #include "common/timer.hpp"
 #include "net/ip6_headers.hpp"
 
@@ -60,34 +61,14 @@
  *
  */
 OT_TOOL_PACKED_BEGIN
-class OptionMpl : public OptionHeader
+class MplOption : public Option
 {
 public:
-    static constexpr uint8_t kType      = 0x6d; // 01 1 01101
-    static constexpr uint8_t kMinLength = 2;
+    static constexpr uint8_t kType    = 0x6d;                 ///< MPL option type - 01 1 01101
+    static constexpr uint8_t kMinSize = (2 + sizeof(Option)); ///< Minimum size (num of bytes) of `MplOption`
 
     /**
-     * This method initializes the MPL header.
-     *
-     */
-    void Init(void)
-    {
-        OptionHeader::SetType(kType);
-        OptionHeader::SetLength(sizeof(*this) - sizeof(OptionHeader));
-        mControl = 0;
-    }
-
-    /**
-     * This method returns the total MPL Option length value including option
-     * header.
-     *
-     * @returns The total IPv6 Option Length.
-     *
-     */
-    uint8_t GetTotalLength(void) const { return OptionHeader::GetLength() + sizeof(OptionHeader); }
-
-    /**
-     * MPL Seed Id lengths.
+     * MPL Seed Id Lengths.
      *
      */
     enum SeedIdLength : uint8_t
@@ -99,6 +80,16 @@
     };
 
     /**
+     * This method initializes the MPL Option.
+     *
+     * The @p aSeedIdLength MUST be either `kSeedIdLength0` or `kSeedIdLength2`. Other values are not supported.
+     *
+     * @param[in] aSeedIdLength   The MPL Seed Id Length.
+     *
+     */
+    void Init(SeedIdLength aSeedIdLength);
+
+    /**
      * This method returns the MPL Seed Id Length value.
      *
      * @returns The MPL Seed Id Length value.
@@ -107,17 +98,6 @@
     SeedIdLength GetSeedIdLength(void) const { return static_cast<SeedIdLength>(mControl & kSeedIdLengthMask); }
 
     /**
-     * This method sets the MPL Seed Id Length value.
-     *
-     * @param[in]  aSeedIdLength  The MPL Seed Length.
-     *
-     */
-    void SetSeedIdLength(SeedIdLength aSeedIdLength)
-    {
-        mControl = static_cast<uint8_t>((mControl & ~kSeedIdLengthMask) | aSeedIdLength);
-    }
-
-    /**
      * This method indicates whether or not the MPL M flag is set.
      *
      * @retval TRUE   If the MPL M flag is set.
@@ -185,6 +165,8 @@
  */
 class Mpl : public InstanceLocator, private NonCopyable
 {
+    friend class ot::TimeTicker;
+
 public:
     /**
      * This constructor initializes the MPL object.
@@ -201,7 +183,7 @@
      * @param[in]  aAddress  A reference to the IPv6 Source Address.
      *
      */
-    void InitOption(OptionMpl &aOption, const Address &aAddress);
+    void InitOption(MplOption &aOption, const Address &aAddress);
 
     /**
      * This method processes an MPL option. When the MPL module acts as an MPL Forwarder
@@ -210,6 +192,7 @@
      * timer expirations for subsequent retransmissions.
      *
      * @param[in]  aMessage    A reference to the message.
+     * @param[in]  aOffset     The offset in @p aMessage to read the MPL option.
      * @param[in]  aAddress    A reference to the IPv6 Source Address.
      * @param[in]  aIsOutbound TRUE if this message was locally generated, FALSE otherwise.
      * @param[out] aReceive    Set to FALSE if the MPL message is a duplicate and must not
@@ -219,59 +202,17 @@
      * @retval kErrorDrop  The MPL message is a duplicate and should be dropped.
      *
      */
-    Error ProcessOption(Message &aMessage, const Address &aAddress, bool aIsOutbound, bool &aReceive);
-
-    /**
-     * This method returns the MPL Seed Id value.
-     *
-     * @returns The MPL Seed Id value.
-     *
-     */
-    uint16_t GetSeedId(void) const { return mSeedId; }
-
-    /**
-     * This method sets the MPL Seed Id value.
-     *
-     * @param[in]  aSeedId  The MPL Seed Id value.
-     *
-     */
-    void SetSeedId(uint16_t aSeedId) { mSeedId = aSeedId; }
-
-    /**
-     * This method sets the IPv6 matching address, that allows to elide MPL Seed Id.
-     *
-     * @param[in] aAddress The reference to the IPv6 matching address.
-     *
-     */
-    void SetMatchingAddress(const Address &aAddress) { mMatchingAddress = &aAddress; }
+    Error ProcessOption(Message &aMessage, uint16_t aOffset, const Address &aAddress, bool aIsOutbound, bool &aReceive);
 
 #if OPENTHREAD_FTD
     /**
-     * This method gets the MPL number of Trickle timer expirations that occur before
-     * terminating the Trickle algorithm's retransmission of a given MPL Data Message.
-     *
-     * @returns The MPL number of Trickle timer expirations.
-     *
-     */
-    uint8_t GetTimerExpirations(void) const { return mTimerExpirations; }
-
-    /**
-     * This method sets the MPL number of Trickle timer expirations that occur before
-     * terminating the Trickle algorithm's retransmission of a given MPL Data Message.
-     *
-     * @param[in]  aTimerExpirations  The number of Trickle timer expirations.
-     *
-     */
-    void SetTimerExpirations(uint8_t aTimerExpirations) { mTimerExpirations = aTimerExpirations; }
-
-    /**
      * This method returns a reference to the buffered message set.
      *
      * @returns A reference to the buffered message set.
      *
      */
     const MessageQueue &GetBufferedMessageSet(void) const { return mBufferedMessageSet; }
-#endif // OPENTHREAD_FTD
+#endif
 
 private:
     static constexpr uint16_t kNumSeedEntries      = OPENTHREAD_CONFIG_MPL_SEED_SET_ENTRIES;
@@ -286,18 +227,16 @@
         uint8_t  mLifetime;
     };
 
-    static void HandleSeedSetTimer(Timer &aTimer);
-    void        HandleSeedSetTimer(void);
-
+    void  HandleTimeTick(void);
     Error UpdateSeedSet(uint16_t aSeedId, uint8_t aSequence);
 
-    SeedEntry      mSeedSet[kNumSeedEntries];
-    const Address *mMatchingAddress;
-    TimerMilli     mSeedSetTimer;
-    uint16_t       mSeedId;
-    uint8_t        mSequence;
+    SeedEntry mSeedSet[kNumSeedEntries];
+    uint8_t   mSequence;
 
 #if OPENTHREAD_FTD
+    static constexpr uint8_t kChildTimerExpirations  = 0; // MPL retransmissions for Children.
+    static constexpr uint8_t kRouterTimerExpirations = 2; // MPL retransmissions for Routers.
+
     struct Metadata
     {
         Error AppendTo(Message &aMessage) const { return aMessage.Append(*this); }
@@ -313,14 +252,14 @@
         uint8_t   mIntervalOffset;
     };
 
-    static void HandleRetransmissionTimer(Timer &aTimer);
-    void        HandleRetransmissionTimer(void);
+    uint8_t GetTimerExpirations(void) const;
+    void    HandleRetransmissionTimer(void);
+    void    AddBufferedMessage(Message &aMessage, uint16_t aSeedId, uint8_t aSequence, bool aIsOutbound);
 
-    void AddBufferedMessage(Message &aMessage, uint16_t aSeedId, uint8_t aSequence, bool aIsOutbound);
+    using RetxTimer = TimerMilliIn<Mpl, &Mpl::HandleRetransmissionTimer>;
 
     MessageQueue mBufferedMessageSet;
-    TimerMilli   mRetransmissionTimer;
-    uint8_t      mTimerExpirations;
+    RetxTimer    mRetransmissionTimer;
 #endif // OPENTHREAD_FTD
 };
 
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
new file mode 100644
index 0000000..ee9cd2d
--- /dev/null
+++ b/src/core/net/nat64_translator.cpp
@@ -0,0 +1,680 @@
+/*
+ *  Copyright (c) 2022, 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 implementation for the NAT64 translator.
+ *
+ */
+
+#include "nat64_translator.hpp"
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+
+#include "common/code_utils.hpp"
+#include "common/locator_getters.hpp"
+#include "common/log.hpp"
+#include "net/checksum.hpp"
+#include "net/ip4_types.hpp"
+#include "net/ip6.hpp"
+
+namespace ot {
+namespace Nat64 {
+
+RegisterLogModule("Nat64");
+
+const char *StateToString(State aState)
+{
+    static const char *const kStateString[] = {
+        "Disabled",
+        "NotRunning",
+        "Idle",
+        "Active",
+    };
+
+    static_assert(0 == kStateDisabled, "kStateDisabled value is incorrect");
+    static_assert(1 == kStateNotRunning, "kStateNotRunning value is incorrect");
+    static_assert(2 == kStateIdle, "kStateIdle value is incorrect");
+    static_assert(3 == kStateActive, "kStateActive value is incorrect");
+
+    return kStateString[aState];
+}
+
+Translator::Translator(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mState(State::kStateDisabled)
+    , mMappingExpirerTimer(aInstance)
+{
+    Random::NonCrypto::FillBuffer(reinterpret_cast<uint8_t *>(&mNextMappingId), sizeof(mNextMappingId));
+
+    mNat64Prefix.Clear();
+    mIp4Cidr.Clear();
+    mMappingExpirerTimer.Start(kAddressMappingIdleTimeoutMsec);
+}
+
+Message *Translator::NewIp4Message(const Message::Settings &aSettings)
+{
+    Message *message = Get<Ip6::Ip6>().NewMessage(sizeof(Ip6::Header) - sizeof(Ip4::Header), aSettings);
+
+    if (message != nullptr)
+    {
+        message->SetType(Message::kTypeIp4);
+    }
+
+    return message;
+}
+
+Error Translator::SendMessage(Message &aMessage)
+{
+    bool   freed  = false;
+    Error  error  = kErrorDrop;
+    Result result = TranslateToIp6(aMessage);
+
+    VerifyOrExit(result == kForward);
+
+    error = Get<Ip6::Ip6>().SendRaw(aMessage, !OPENTHREAD_CONFIG_IP6_ALLOW_LOOP_BACK_HOST_DATAGRAMS);
+    freed = true;
+
+exit:
+    if (!freed)
+    {
+        aMessage.Free();
+    }
+
+    return error;
+}
+
+Translator::Result Translator::TranslateFromIp6(Message &aMessage)
+{
+    Result                res        = kDrop;
+    ErrorCounters::Reason dropReason = ErrorCounters::kUnknown;
+    Ip6::Header           ip6Header;
+    Ip4::Header           ip4Header;
+    AddressMapping       *mapping = nullptr;
+
+    if (mIp4Cidr.mLength == 0 || !mNat64Prefix.IsValidNat64())
+    {
+        ExitNow(res = kNotTranslated);
+    }
+
+    // ParseFrom will do basic checks for the message, including the message length and IP protocol version.
+    if (ip6Header.ParseFrom(aMessage) != kErrorNone)
+    {
+        LogWarn("outgoing datagram is not a valid IPv6 datagram, drop");
+        dropReason = ErrorCounters::Reason::kIllegalPacket;
+        ExitNow(res = kDrop);
+    }
+
+    if (!ip6Header.GetDestination().MatchesPrefix(mNat64Prefix))
+    {
+        ExitNow(res = kNotTranslated);
+    }
+
+    mapping = FindOrAllocateMapping(ip6Header.GetSource());
+    if (mapping == nullptr)
+    {
+        LogWarn("failed to get a mapping for %s (mapping pool full?)", ip6Header.GetSource().ToString().AsCString());
+        dropReason = ErrorCounters::Reason::kNoMapping;
+        ExitNow(res = kDrop);
+    }
+
+    aMessage.RemoveHeader(sizeof(Ip6::Header));
+
+    ip4Header.Clear();
+    ip4Header.InitVersionIhl();
+    ip4Header.SetSource(mapping->mIp4);
+    ip4Header.GetDestination().ExtractFromIp6Address(mNat64Prefix.mLength, ip6Header.GetDestination());
+    ip4Header.SetTtl(ip6Header.GetHopLimit());
+    ip4Header.SetIdentification(0);
+
+    switch (ip6Header.GetNextHeader())
+    {
+    case Ip6::kProtoUdp:
+        ip4Header.SetProtocol(Ip4::kProtoUdp);
+        res = kForward;
+        break;
+    case Ip6::kProtoTcp:
+        ip4Header.SetProtocol(Ip4::kProtoTcp);
+        res = kForward;
+        break;
+    case Ip6::kProtoIcmp6:
+        ip4Header.SetProtocol(Ip4::kProtoIcmp);
+        SuccessOrExit(TranslateIcmp6(aMessage));
+        res = kForward;
+        break;
+    default:
+        dropReason = ErrorCounters::Reason::kUnsupportedProto;
+        ExitNow(res = kDrop);
+    }
+
+    // res here must be kForward based on the switch above.
+    // TODO: Implement the logic for replying ICMP messages.
+    ip4Header.SetTotalLength(sizeof(Ip4::Header) + aMessage.GetLength() - aMessage.GetOffset());
+    Checksum::UpdateMessageChecksum(aMessage, ip4Header.GetSource(), ip4Header.GetDestination(),
+                                    ip4Header.GetProtocol());
+    Checksum::UpdateIp4HeaderChecksum(ip4Header);
+    if (aMessage.Prepend(ip4Header) != kErrorNone)
+    {
+        // This should never happen since the IPv4 header is shorter than the IPv6 header.
+        LogCrit("failed to prepend IPv4 head to translated message");
+        ExitNow(res = kDrop);
+    }
+    aMessage.SetType(Message::kTypeIp4);
+    mCounters.Count6To4Packet(ip6Header.GetNextHeader(), ip6Header.GetPayloadLength());
+    mapping->mCounters.Count6To4Packet(ip6Header.GetNextHeader(), ip6Header.GetPayloadLength());
+
+exit:
+    if (res == Result::kDrop)
+    {
+        mErrorCounters.Count6To4(dropReason);
+    }
+    return res;
+}
+
+Translator::Result Translator::TranslateToIp6(Message &aMessage)
+{
+    Result                res        = Result::kDrop;
+    ErrorCounters::Reason dropReason = ErrorCounters::kUnknown;
+    Ip6::Header           ip6Header;
+    Ip4::Header           ip4Header;
+    AddressMapping       *mapping = nullptr;
+
+    // Ip6::Header::ParseFrom may return an error value when the incoming message is an IPv4 datagram.
+    // If the message is already an IPv6 datagram, forward it directly.
+    VerifyOrExit(ip6Header.ParseFrom(aMessage) != kErrorNone, res = kNotTranslated);
+
+    if (mIp4Cidr.mLength == 0)
+    {
+        // The NAT64 translation is bypassed (will be handled externally)
+        LogWarn("incoming message is an IPv4 datagram but no IPv4 CIDR for NAT64 configured, drop");
+        ExitNow(res = kForward);
+    }
+
+    if (!mNat64Prefix.IsValidNat64())
+    {
+        LogWarn("incoming message is an IPv4 datagram but no NAT64 prefix configured, drop");
+        ExitNow(res = kDrop);
+    }
+
+    if (ip4Header.ParseFrom(aMessage) != kErrorNone)
+    {
+        LogWarn("incoming message is neither IPv4 nor an IPv6 datagram, drop");
+        dropReason = ErrorCounters::Reason::kIllegalPacket;
+        ExitNow(res = kDrop);
+    }
+
+    mapping = FindMapping(ip4Header.GetDestination());
+    if (mapping == nullptr)
+    {
+        LogWarn("no mapping found for the IPv4 address");
+        dropReason = ErrorCounters::Reason::kNoMapping;
+        ExitNow(res = kDrop);
+    }
+
+    aMessage.RemoveHeader(sizeof(Ip4::Header));
+
+    ip6Header.Clear();
+    ip6Header.InitVersionTrafficClassFlow();
+    ip6Header.GetSource().SynthesizeFromIp4Address(mNat64Prefix, ip4Header.GetSource());
+    ip6Header.SetDestination(mapping->mIp6);
+    ip6Header.SetFlow(0);
+    ip6Header.SetHopLimit(ip4Header.GetTtl());
+
+    // Note: TCP and UDP are the same for both IPv4 and IPv6 except for the checksum calculation, we will update the
+    // checksum in the payload later. However, we need to translate ICMPv6 messages to ICMP messages in IPv4.
+    switch (ip4Header.GetProtocol())
+    {
+    case Ip4::kProtoUdp:
+        ip6Header.SetNextHeader(Ip6::kProtoUdp);
+        res = kForward;
+        break;
+    case Ip4::kProtoTcp:
+        ip6Header.SetNextHeader(Ip6::kProtoTcp);
+        res = kForward;
+        break;
+    case Ip4::kProtoIcmp:
+        ip6Header.SetNextHeader(Ip6::kProtoIcmp6);
+        SuccessOrExit(TranslateIcmp4(aMessage));
+        res = kForward;
+        break;
+    default:
+        dropReason = ErrorCounters::Reason::kUnsupportedProto;
+        ExitNow(res = kDrop);
+    }
+
+    // res here must be kForward based on the switch above.
+    // TODO: Implement the logic for replying ICMP datagrams.
+    ip6Header.SetPayloadLength(aMessage.GetLength() - aMessage.GetOffset());
+    Checksum::UpdateMessageChecksum(aMessage, ip6Header.GetSource(), ip6Header.GetDestination(),
+                                    ip6Header.GetNextHeader());
+    if (aMessage.Prepend(ip6Header) != kErrorNone)
+    {
+        // This might happen when the platform failed to reserve enough space before the original IPv4 datagram.
+        LogWarn("failed to prepend IPv6 head to translated message");
+        ExitNow(res = kDrop);
+    }
+    aMessage.SetType(Message::kTypeIp6);
+    mCounters.Count4To6Packet(ip4Header.GetProtocol(), ip4Header.GetTotalLength() - sizeof(ip4Header));
+    mapping->mCounters.Count4To6Packet(ip4Header.GetProtocol(), ip4Header.GetTotalLength() - sizeof(ip4Header));
+
+exit:
+    if (res == Result::kDrop)
+    {
+        mErrorCounters.Count4To6(dropReason);
+    }
+
+    return res;
+}
+
+Translator::AddressMapping::InfoString Translator::AddressMapping::ToString(void) const
+{
+    InfoString string;
+
+    string.Append("%s -> %s", mIp6.ToString().AsCString(), mIp4.ToString().AsCString());
+
+    return string;
+}
+
+void Translator::AddressMapping::CopyTo(otNat64AddressMapping &aMapping, TimeMilli aNow) const
+{
+    aMapping.mId       = mId;
+    aMapping.mIp4      = mIp4;
+    aMapping.mIp6      = mIp6;
+    aMapping.mCounters = mCounters;
+
+    // We are removing expired mappings lazily, and an expired mapping might become active again before actually
+    // removed. Report the mapping to be "just expired" to avoid confusion.
+    if (mExpiry < aNow)
+    {
+        aMapping.mRemainingTimeMs = 0;
+    }
+    else
+    {
+        aMapping.mRemainingTimeMs = mExpiry - aNow;
+    }
+}
+
+void Translator::ReleaseMapping(AddressMapping &aMapping)
+{
+    IgnoreError(mIp4AddressPool.PushBack(aMapping.mIp4));
+    mAddressMappingPool.Free(aMapping);
+    LogInfo("mapping removed: %s", aMapping.ToString().AsCString());
+}
+
+uint16_t Translator::ReleaseMappings(LinkedList<AddressMapping> &aMappings)
+{
+    uint16_t numRemoved = 0;
+
+    for (AddressMapping *mapping = aMappings.Pop(); mapping != nullptr; mapping = aMappings.Pop())
+    {
+        numRemoved++;
+        ReleaseMapping(*mapping);
+    }
+
+    return numRemoved;
+}
+
+uint16_t Translator::ReleaseExpiredMappings(void)
+{
+    LinkedList<AddressMapping> idleMappings;
+
+    mActiveAddressMappings.RemoveAllMatching(TimerMilli::GetNow(), idleMappings);
+
+    return ReleaseMappings(idleMappings);
+}
+
+Translator::AddressMapping *Translator::AllocateMapping(const Ip6::Address &aIp6Addr)
+{
+    AddressMapping *mapping = nullptr;
+
+    // The address pool will be no larger than the mapping pool, so checking the address pool is enough.
+    if (mIp4AddressPool.IsEmpty())
+    {
+        // ReleaseExpiredMappings returns the number of mappings removed.
+        VerifyOrExit(ReleaseExpiredMappings() > 0);
+    }
+
+    mapping = mAddressMappingPool.Allocate();
+    // We should get a valid item since address pool is no larger than the mapping pool, and the address pool is not
+    // empty.
+    VerifyOrExit(mapping != nullptr);
+
+    mActiveAddressMappings.Push(*mapping);
+    mapping->mId  = ++mNextMappingId;
+    mapping->mIp6 = aIp6Addr;
+    // PopBack must return a valid address since it is not empty.
+    mapping->mIp4 = *mIp4AddressPool.PopBack();
+    mapping->Touch(TimerMilli::GetNow());
+    LogInfo("mapping created: %s", mapping->ToString().AsCString());
+
+exit:
+    return mapping;
+}
+
+Translator::AddressMapping *Translator::FindOrAllocateMapping(const Ip6::Address &aIp6Addr)
+{
+    AddressMapping *mapping = mActiveAddressMappings.FindMatching(aIp6Addr);
+
+    // Exit if we found a valid mapping.
+    VerifyOrExit(mapping == nullptr);
+
+    mapping = AllocateMapping(aIp6Addr);
+
+exit:
+    return mapping;
+}
+
+Translator::AddressMapping *Translator::FindMapping(const Ip4::Address &aIp4Addr)
+{
+    AddressMapping *mapping = mActiveAddressMappings.FindMatching(aIp4Addr);
+
+    if (mapping != nullptr)
+    {
+        mapping->Touch(TimerMilli::GetNow());
+    }
+    return mapping;
+}
+
+Error Translator::TranslateIcmp4(Message &aMessage)
+{
+    Error             err = kErrorNone;
+    Ip4::Icmp::Header icmp4Header;
+    Ip6::Icmp::Header icmp6Header;
+
+    // TODO: Implement the translation of other ICMP messages.
+
+    // Note: The caller consumed the IP header, so the ICMP header is at offset 0.
+    SuccessOrExit(err = aMessage.Read(0, icmp4Header));
+    switch (icmp4Header.GetType())
+    {
+    case Ip4::Icmp::Header::Type::kTypeEchoReply:
+    {
+        // 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);
+        aMessage.Write(0, icmp6Header);
+        break;
+    }
+    default:
+        err = kErrorInvalidArgs;
+        break;
+    }
+
+exit:
+    return err;
+}
+
+Error Translator::TranslateIcmp6(Message &aMessage)
+{
+    Error             err = kErrorNone;
+    Ip4::Icmp::Header icmp4Header;
+    Ip6::Icmp::Header icmp6Header;
+
+    // TODO: Implement the translation of other ICMP messages.
+
+    // Note: The caller have consumed the IP header, so the ICMP header is at offset 0.
+    SuccessOrExit(err = aMessage.Read(0, icmp6Header));
+    switch (icmp6Header.GetType())
+    {
+    case Ip6::Icmp::Header::Type::kTypeEchoRequest:
+    {
+        // 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);
+        aMessage.Write(0, icmp4Header);
+        break;
+    }
+    default:
+        err = kErrorInvalidArgs;
+        break;
+    }
+
+exit:
+    return err;
+}
+
+Error Translator::SetIp4Cidr(const Ip4::Cidr &aCidr)
+{
+    Error err = kErrorNone;
+
+    uint32_t numberOfHosts;
+    uint32_t hostIdBegin;
+
+    VerifyOrExit(aCidr.mLength > 0 && aCidr.mLength <= 32, err = kErrorInvalidArgs);
+
+    VerifyOrExit(mIp4Cidr != aCidr);
+
+    // Avoid using the 0s and 1s in the host id of an address, but what if the user provides us with /32 or /31
+    // addresses?
+    if (aCidr.mLength == 32)
+    {
+        hostIdBegin   = 0;
+        numberOfHosts = 1;
+    }
+    else if (aCidr.mLength == 31)
+    {
+        hostIdBegin   = 0;
+        numberOfHosts = 2;
+    }
+    else
+    {
+        hostIdBegin   = 1;
+        numberOfHosts = static_cast<uint32_t>((1 << (Ip4::Address::kSize * 8 - aCidr.mLength)) - 2);
+    }
+    numberOfHosts = OT_MIN(numberOfHosts, kAddressMappingPoolSize);
+
+    mAddressMappingPool.FreeAll();
+    mActiveAddressMappings.Clear();
+    mIp4AddressPool.Clear();
+
+    for (uint32_t i = 0; i < numberOfHosts; i++)
+    {
+        Ip4::Address addr;
+
+        addr.SynthesizeFromCidrAndHost(aCidr, i + hostIdBegin);
+        IgnoreError(mIp4AddressPool.PushBack(addr));
+    }
+
+    LogInfo("IPv4 CIDR for NAT64: %s (actual address pool: %s - %s, %u addresses)", aCidr.ToString().AsCString(),
+            mIp4AddressPool.Front()->ToString().AsCString(), mIp4AddressPool.Back()->ToString().AsCString(),
+            numberOfHosts);
+    mIp4Cidr = aCidr;
+
+    UpdateState();
+
+exit:
+    return err;
+}
+
+void Translator::SetNat64Prefix(const Ip6::Prefix &aNat64Prefix)
+{
+    if (aNat64Prefix.GetLength() == 0)
+    {
+        ClearNat64Prefix();
+    }
+    else if (mNat64Prefix != aNat64Prefix)
+    {
+        LogInfo("IPv6 Prefix for NAT64 updated to %s", aNat64Prefix.ToString().AsCString());
+        mNat64Prefix = aNat64Prefix;
+        UpdateState();
+    }
+}
+
+void Translator::ClearNat64Prefix(void)
+{
+    VerifyOrExit(mNat64Prefix.GetLength() != 0);
+    mNat64Prefix.Clear();
+    LogInfo("IPv6 Prefix for NAT64 cleared");
+    UpdateState();
+
+exit:
+    return;
+}
+
+void Translator::HandleMappingExpirerTimer(void)
+{
+    LogInfo("Released %d expired mappings", ReleaseExpiredMappings());
+    mMappingExpirerTimer.Start(kAddressMappingIdleTimeoutMsec);
+}
+
+void Translator::InitAddressMappingIterator(AddressMappingIterator &aIterator)
+{
+    aIterator.mPtr = mActiveAddressMappings.GetHead();
+}
+
+Error Translator::GetNextAddressMapping(AddressMappingIterator &aIterator, otNat64AddressMapping &aMapping)
+{
+    Error           err  = kErrorNotFound;
+    TimeMilli       now  = TimerMilli::GetNow();
+    AddressMapping *item = static_cast<AddressMapping *>(aIterator.mPtr);
+
+    VerifyOrExit(item != nullptr);
+
+    item->CopyTo(aMapping, now);
+    aIterator.mPtr = item->GetNext();
+    err            = kErrorNone;
+
+exit:
+    return err;
+}
+
+Error Translator::GetIp4Cidr(Ip4::Cidr &aCidr)
+{
+    Error err = kErrorNone;
+
+    VerifyOrExit(mIp4Cidr.mLength > 0, err = kErrorNotFound);
+    aCidr = mIp4Cidr;
+
+exit:
+    return err;
+}
+
+Error Translator::GetIp6Prefix(Ip6::Prefix &aPrefix)
+{
+    Error err = kErrorNone;
+
+    VerifyOrExit(mNat64Prefix.mLength > 0, err = kErrorNotFound);
+    aPrefix = mNat64Prefix;
+
+exit:
+    return err;
+}
+
+void Translator::ProtocolCounters::Count6To4Packet(uint8_t aProtocol, uint64_t aPacketSize)
+{
+    switch (aProtocol)
+    {
+    case Ip6::kProtoUdp:
+        mUdp.m6To4Packets++;
+        mUdp.m6To4Bytes += aPacketSize;
+        break;
+    case Ip6::kProtoTcp:
+        mTcp.m6To4Packets++;
+        mTcp.m6To4Bytes += aPacketSize;
+        break;
+    case Ip6::kProtoIcmp6:
+        mIcmp.m6To4Packets++;
+        mIcmp.m6To4Bytes += aPacketSize;
+        break;
+    }
+
+    mTotal.m6To4Packets++;
+    mTotal.m6To4Bytes += aPacketSize;
+}
+
+void Translator::ProtocolCounters::Count4To6Packet(uint8_t aProtocol, uint64_t aPacketSize)
+{
+    switch (aProtocol)
+    {
+    case Ip4::kProtoUdp:
+        mUdp.m4To6Packets++;
+        mUdp.m4To6Bytes += aPacketSize;
+        break;
+    case Ip4::kProtoTcp:
+        mTcp.m4To6Packets++;
+        mTcp.m4To6Bytes += aPacketSize;
+        break;
+    case Ip4::kProtoIcmp:
+        mIcmp.m4To6Packets++;
+        mIcmp.m4To6Bytes += aPacketSize;
+        break;
+    }
+
+    mTotal.m4To6Packets++;
+    mTotal.m4To6Bytes += aPacketSize;
+}
+
+void Translator::UpdateState(void)
+{
+    State newState;
+
+    if (mEnabled)
+    {
+        if (mIp4Cidr.mLength > 0 && mNat64Prefix.IsValidNat64())
+        {
+            newState = kStateActive;
+        }
+        else
+        {
+            newState = kStateNotRunning;
+        }
+    }
+    else
+    {
+        newState = kStateDisabled;
+    }
+
+    SuccessOrExit(Get<Notifier>().Update(mState, newState, kEventNat64TranslatorStateChanged));
+    LogInfo("NAT64 translator is now %s", StateToString(mState));
+
+exit:
+    return;
+}
+
+void Translator::SetEnabled(bool aEnabled)
+{
+    VerifyOrExit(mEnabled != aEnabled);
+    mEnabled = aEnabled;
+
+    if (!aEnabled)
+    {
+        ReleaseMappings(mActiveAddressMappings);
+    }
+
+    UpdateState();
+
+exit:
+    return;
+}
+
+} // namespace Nat64
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
diff --git a/src/core/net/nat64_translator.hpp b/src/core/net/nat64_translator.hpp
new file mode 100644
index 0000000..1ec16a4
--- /dev/null
+++ b/src/core/net/nat64_translator.hpp
@@ -0,0 +1,414 @@
+/*
+ *  Copyright (c) 2022, 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 definitions for the NAT64 translator.
+ *
+ */
+
+#ifndef NAT64_TRANSLATOR_HPP_
+#define NAT64_TRANSLATOR_HPP_
+
+#include "openthread-core-config.h"
+
+#include "common/array.hpp"
+#include "common/linked_list.hpp"
+#include "common/locator.hpp"
+#include "common/pool.hpp"
+#include "common/timer.hpp"
+#include "net/ip4_types.hpp"
+#include "net/ip6.hpp"
+
+namespace ot {
+namespace Nat64 {
+
+enum State : uint8_t
+{
+    kStateDisabled   = OT_NAT64_STATE_DISABLED,    ///< The component is disabled.
+    kStateNotRunning = OT_NAT64_STATE_NOT_RUNNING, ///< The component is enabled, but is not running.
+    kStateIdle       = OT_NAT64_STATE_IDLE,        ///< NAT64 is enabled, but this BR is not an active NAT64 BR.
+    kStateActive     = OT_NAT64_STATE_ACTIVE,      ///< The component is running.
+};
+
+/**
+ * This function converts a `State` into a string.
+ *
+ * @param[in]  aState     A state.
+ *
+ * @returns  A string representation of @p aState.
+ *
+ */
+const char *StateToString(State aState);
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+
+/**
+ * This class implements the NAT64 translator.
+ *
+ */
+class Translator : public InstanceLocator, private NonCopyable
+{
+public:
+    static constexpr uint32_t kAddressMappingIdleTimeoutMsec =
+        OPENTHREAD_CONFIG_NAT64_IDLE_TIMEOUT_SECONDS * Time::kOneSecondInMsec;
+    static constexpr uint32_t kAddressMappingPoolSize = OPENTHREAD_CONFIG_NAT64_MAX_MAPPINGS;
+
+    typedef otNat64AddressMappingIterator AddressMappingIterator; ///< Address mapping Iterator.
+
+    /**
+     * The possible results of NAT64 translation.
+     *
+     */
+    enum Result : uint8_t
+    {
+        kNotTranslated, ///< The message is not translated, it might be sending to an non-nat64 prefix (for outgoing
+                        ///< datagrams), or it is already an IPv6 message (for incoming datagrams).
+        kForward,       ///< Message is successfully translated, the caller should continue forwarding the translated
+                        ///< datagram.
+        kDrop,          ///< The caller should drop the datagram silently.
+    };
+
+    /**
+     * Represents the counters for the protocols supported by NAT64.
+     *
+     */
+    class ProtocolCounters : public otNat64ProtocolCounters, public Clearable<ProtocolCounters>
+    {
+    public:
+        /**
+         * Adds the packet to the counter for the given IPv6 protocol.
+         *
+         * @param[in] aProtocol    The protocol of the packet.
+         * @param[in] aPacketSize  The size of the packet.
+         *
+         */
+        void Count6To4Packet(uint8_t aProtocol, uint64_t aPacketSize);
+
+        /**
+         * Adds the packet to the counter for the given IPv4 protocol.
+         *
+         * @param[in] aProtocol    The protocol of the packet.
+         * @param[in] aPacketSize  The size of the packet.
+         *
+         */
+        void Count4To6Packet(uint8_t aProtocol, uint64_t aPacketSize);
+    };
+
+    /**
+     * Represents the counters of dropped packets due to errors when handling NAT64 packets.
+     *
+     */
+    class ErrorCounters : public otNat64ErrorCounters, public Clearable<otNat64ErrorCounters>
+    {
+    public:
+        enum Reason : uint8_t
+        {
+            kUnknown          = OT_NAT64_DROP_REASON_UNKNOWN,
+            kIllegalPacket    = OT_NAT64_DROP_REASON_ILLEGAL_PACKET,
+            kUnsupportedProto = OT_NAT64_DROP_REASON_UNSUPPORTED_PROTO,
+            kNoMapping        = OT_NAT64_DROP_REASON_NO_MAPPING,
+        };
+
+        /**
+         * Adds the counter for the given reason when translating an IPv4 datagram.
+         *
+         * @param[in] aReason    The reason of packet drop.
+         *
+         */
+        void Count4To6(Reason aReason) { mCount4To6[aReason]++; }
+
+        /**
+         * Adds the counter for the given reason when translating an IPv6 datagram.
+         *
+         * @param[in] aReason    The reason of packet drop.
+         *
+         */
+        void Count6To4(Reason aReason) { mCount6To4[aReason]++; }
+    };
+
+    /**
+     * This constructor initializes the NAT64 translator.
+     *
+     */
+    explicit Translator(Instance &aInstance);
+
+    /**
+     * Set the state of NAT64 translator.
+     *
+     * Note: Disabling the translator will invalidate all address mappings.
+     *
+     * @param[in]  aEnabled   A boolean to enable/disable NAT64 translator.
+     *
+     */
+    void SetEnabled(bool aEnabled);
+
+    /**
+     * Gets the state of NAT64 translator.
+     *
+     * @retval  kNat64StateDisabled  The translator is disabled.
+     * @retval  kNat64StateIdle      The translator is not configured with a valid NAT64 prefix and a CIDR.
+     * @retval  kNat64StateActive    The translator is translating packets.
+     *
+     */
+    State GetState(void) const { return mState; }
+
+    /**
+     * This method translates an IPv4 datagram to an IPv6 datagram and sends it via Thread interface.
+     *
+     * The caller transfers ownership of @p aMessage when making this call. OpenThread will free @p aMessage when
+     * processing is complete, including when a value other than `kErrorNone` is returned.
+     *
+     * @param[in]  aMessage          A reference to the message.
+     *
+     * @retval kErrorNone     Successfully processed the message.
+     * @retval kErrorDrop     Message was well-formed but not fully processed due to datagram processing rules.
+     * @retval kErrorNoBufs   Could not allocate necessary message buffers when processing the datagram.
+     * @retval kErrorNoRoute  No route to host.
+     * @retval kErrorParse    Encountered a malformed header when processing the message.
+     *
+     */
+    Error SendMessage(Message &aMessage);
+
+    /**
+     * Allocate a new message buffer for sending an IPv4 message (which will be translated into an IPv6 datagram by
+     * NAT64 later). Message buffers allocated by this function will have 20 bytes (The differences between the size of
+     * IPv6 headers and the size of IPv4 headers) reserved.
+     *
+     * @param[in]  aSettings  The message settings.
+     *
+     * @returns A pointer to the message buffer or NULL if no message buffers are available or parameters are invalid.
+     *
+     */
+    Message *NewIp4Message(const Message::Settings &aSettings);
+
+    /**
+     * Translates an IPv4 datagram to IPv6 datagram. Note the datagram and datagramLength might be adjusted.
+     * Note the message can have 20 bytes reserved before the message to avoid potential copy operations. If the message
+     * is already an IPv6 datagram, `Result::kNotTranslated` will be returned and @p aMessage won't be modified.
+     *
+     * @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 continue forwarding the datagram.
+     * @retval kDrop          The caller should drop the datagram silently.
+     *
+     */
+    Result TranslateToIp6(Message &message);
+
+    /**
+     * Translates an IPv6 datagram to IPv4 datagram. Note the datagram and datagramLength might be adjusted.
+     * If the message is not targeted to NAT64-mapped address, `Result::kNotTranslated` will be returned and @p aMessage
+     * won't be modified.
+     *
+     * @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 continue forwarding the datagram.
+     * @retval kDrop          The caller should drop the datagram silently.
+     *
+     */
+    Result TranslateFromIp6(Message &aMessage);
+
+    /**
+     * Sets the CIDR used when setting the source address of the outgoing translated IPv4 datagrams. A valid CIDR must
+     * have a non-zero prefix length.
+     *
+     * @note The actual addresses pool is limited by the size of the mapping pool and the number of addresses available
+     * in the CIDR block. If the provided is a valid IPv4 CIDR for NAT64, and it is different from the one already
+     * configured, the NAT64 translator will be reset and all existing sessions will be expired.
+     *
+     * @param[in] aCidr the CIDR for the sources of the translated datagrams.
+     *
+     * @retval  kErrorInvalidArgs    The the given CIDR a valid CIDR for NAT64.
+     * @retval  kErrorNone           Successfully enabled/disabled the NAT64 translator.
+     *
+     */
+    Error SetIp4Cidr(const Ip4::Cidr &aCidr);
+
+    /**
+     * Sets the prefix of NAT64-mapped addresses in the thread network. The address mapping table will not be cleared.
+     * This function equals to `ClearNat64Prefix` when an empty prefix is provided.
+     *
+     * @param[in] aNat64Prefix The prefix of the NAT64-mapped addresses.
+     *
+     */
+    void SetNat64Prefix(const Ip6::Prefix &aNat64Prefix);
+
+    /**
+     * Clear the prefix of NAT64-mapped addresses in the thread network. The address mapping table will not be cleared.
+     * The translator will return kNotTranslated for all IPv6 datagrams and kDrop for all IPv4 datagrams.
+     *
+     */
+    void ClearNat64Prefix(void);
+
+    /**
+     * Initializes an `otNat64AddressMappingIterator`.
+     *
+     * An iterator MUST be initialized before it is used.
+     *
+     * An iterator can be initialized again to restart from the beginning of the mapping info.
+     *
+     * @param[out] aIterator  An iterator to initialize.
+     *
+     */
+    void InitAddressMappingIterator(AddressMappingIterator &aIterator);
+
+    /**
+     * Gets the next AddressMapping info (using an iterator).
+     *
+     * @param[in,out]  aIterator      The iterator. On success the iterator will be updated to point to next NAT64
+     *                                address mapping record. To get the first entry the iterator should be set to
+     *                                OT_NAT64_ADDRESS_MAPPING_ITERATOR_INIT.
+     * @param[out]     aMapping       An `otNat64AddressMapping` where information of next NAT64 address mapping record
+     *                                is placed (on success).
+     *
+     * @retval kErrorNone      Successfully found the next NAT64 address mapping info (@p aMapping was successfully
+     *                         updated).
+     * @retval kErrorNotFound  No subsequent NAT64 address mapping info was found.
+     *
+     */
+    Error GetNextAddressMapping(AddressMappingIterator &aIterator, otNat64AddressMapping &aMapping);
+
+    /**
+     * Gets the NAT64 translator counters.
+     *
+     * The counters are initialized to zero when the OpenThread instance is initialized.
+     *
+     * @param[out] aCounters A `ProtocolCounters` where the counters of NAT64 translator will be placed.
+     *
+     */
+    void GetCounters(ProtocolCounters &aCounters) const { aCounters = mCounters; }
+
+    /**
+     * Gets the NAT64 translator error counters.
+     *
+     * The counters are initialized to zero when the OpenThread instance is initialized.
+     *
+     * @param[out] aCounters  An `ErrorCounters` where the counters of NAT64 translator will be placed.
+     *
+     */
+    void GetErrorCounters(ErrorCounters &aCounters) const { aCounters = mErrorCounters; }
+
+    /**
+     * Gets the configured CIDR in the NAT64 translator.
+     *
+     * @param[out] aCidr        The `Ip4::Cidr` Where the configured CIDR will be placed.
+     *
+     * @retval kErrorNone       @p aCidr is set to the configured CIDR.
+     * @retval kErrorNotFound   The translator is not configured with an IPv4 CIDR.
+     *
+     */
+    Error GetIp4Cidr(Ip4::Cidr &aCidr);
+
+    /**
+     * Gets the configured IPv6 prefix in the NAT64 translator.
+     *
+     * @param[out] aPrefix      The `Ip6::Prefix` where the configured NAT64 prefix will be placed.
+     *
+     * @retval kErrorNone       @p aPrefix is set to the configured prefix.
+     * @retval kErrorNotFound   The translator is not configured with an IPv6 prefix.
+     *
+     */
+    Error GetIp6Prefix(Ip6::Prefix &aPrefix);
+
+private:
+    class AddressMapping : public LinkedListEntry<AddressMapping>
+    {
+    public:
+        friend class LinkedListEntry<AddressMapping>;
+        friend class LinkedList<AddressMapping>;
+
+        typedef String<Ip6::Address::kInfoStringSize + Ip4::Address::kAddressStringSize + 4> InfoString;
+
+        void       Touch(TimeMilli aNow) { mExpiry = aNow + kAddressMappingIdleTimeoutMsec; }
+        InfoString ToString(void) const;
+        void       CopyTo(otNat64AddressMapping &aMapping, TimeMilli aNow) const;
+
+        uint64_t mId; // The unique id for a mapping session.
+
+        Ip4::Address mIp4;
+        Ip6::Address mIp6;
+        TimeMilli    mExpiry; // The timestamp when this mapping expires, in milliseconds.
+
+        ProtocolCounters mCounters;
+
+    private:
+        bool Matches(const Ip4::Address &aIp4) const { return mIp4 == aIp4; }
+        bool Matches(const Ip6::Address &aIp6) const { return mIp6 == aIp6; }
+        bool Matches(const TimeMilli aNow) const { return mExpiry < aNow; }
+
+        AddressMapping *mNext;
+    };
+
+    Error TranslateIcmp4(Message &aMessage);
+    Error TranslateIcmp6(Message &aMessage);
+
+    uint16_t        ReleaseMappings(LinkedList<AddressMapping> &aMappings);
+    void            ReleaseMapping(AddressMapping &aMapping);
+    uint16_t        ReleaseExpiredMappings(void);
+    AddressMapping *AllocateMapping(const Ip6::Address &aIp6Addr);
+    AddressMapping *FindOrAllocateMapping(const Ip6::Address &aIp6Addr);
+    AddressMapping *FindMapping(const Ip4::Address &aIp4Addr);
+
+    void HandleMappingExpirerTimer(void);
+
+    using MappingTimer = TimerMilliIn<Translator, &Translator::HandleMappingExpirerTimer>;
+
+    void UpdateState(void);
+
+    bool  mEnabled;
+    State mState;
+
+    uint64_t mNextMappingId;
+
+    Array<Ip4::Address, kAddressMappingPoolSize>  mIp4AddressPool;
+    Pool<AddressMapping, kAddressMappingPoolSize> mAddressMappingPool;
+    LinkedList<AddressMapping>                    mActiveAddressMappings;
+
+    Ip6::Prefix mNat64Prefix;
+    Ip4::Cidr   mIp4Cidr;
+
+    MappingTimer mMappingExpirerTimer;
+
+    ProtocolCounters mCounters;
+    ErrorCounters    mErrorCounters;
+};
+#endif // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+
+} // namespace Nat64
+
+DefineMapEnum(otNat64State, Nat64::State);
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+DefineCoreType(otNat64ProtocolCounters, Nat64::Translator::ProtocolCounters);
+DefineCoreType(otNat64ErrorCounters, Nat64::Translator::ErrorCounters);
+#endif
+
+} // namespace ot
+
+#endif // NAT64_TRANSLATOR_HPP_
diff --git a/src/core/net/nd6.cpp b/src/core/net/nd6.cpp
index 5b57e1b..cc5e6bf 100644
--- a/src/core/net/nd6.cpp
+++ b/src/core/net/nd6.cpp
@@ -66,10 +66,7 @@
     return reinterpret_cast<const Option *>(reinterpret_cast<const uint8_t *>(aOption) + aOption->GetSize());
 }
 
-void Option::Iterator::Advance(void)
-{
-    mOption = (mOption != nullptr) ? Validate(Next(mOption)) : nullptr;
-}
+void Option::Iterator::Advance(void) { mOption = (mOption != nullptr) ? Validate(Next(mOption)) : nullptr; }
 
 const Option *Option::Iterator::Validate(const Option *aOption) const
 {
@@ -99,10 +96,7 @@
     mPrefix       = AsCoreType(&aPrefix.mPrefix);
 }
 
-void PrefixInfoOption::GetPrefix(Prefix &aPrefix) const
-{
-    aPrefix.Set(mPrefix.GetBytes(), mPrefixLength);
-}
+void PrefixInfoOption::GetPrefix(Prefix &aPrefix) const { aPrefix.Set(mPrefix.GetBytes(), mPrefixLength); }
 
 bool PrefixInfoOption::IsValid(void) const
 {
@@ -137,10 +131,7 @@
     memcpy(GetPrefixBytes(), aPrefix.GetBytes(), aPrefix.GetBytesSize());
 }
 
-void RouteInfoOption::GetPrefix(Prefix &aPrefix) const
-{
-    aPrefix.Set(GetPrefixBytes(), mPrefixLength);
-}
+void RouteInfoOption::GetPrefix(Prefix &aPrefix) const { aPrefix.Set(GetPrefixBytes(), mPrefixLength); }
 
 bool RouteInfoOption::IsValid(void) const
 {
@@ -215,7 +206,7 @@
     // returns `nullptr`. The returned option needs to be
     // initialized and populated by the caller.
 
-    Option * option    = nullptr;
+    Option  *option    = nullptr;
     uint32_t newLength = mData.GetLength();
 
     newLength += aOptionSize;
@@ -249,7 +240,7 @@
     return error;
 }
 
-Error RouterAdvertMessage::AppendRouteInfoOption(const Prefix &  aPrefix,
+Error RouterAdvertMessage::AppendRouteInfoOption(const Prefix   &aPrefix,
                                                  uint32_t        aRouteLifetime,
                                                  RoutePreference aPreference)
 {
@@ -269,7 +260,7 @@
 }
 
 //----------------------------------------------------------------------------------------------------------------------
-// RouterAdvMessage
+// RouterSolicitMessage
 
 RouterSolicitMessage::RouterSolicitMessage(void)
 {
@@ -277,6 +268,30 @@
     mHeader.SetType(Icmp::Header::kTypeRouterSolicit);
 }
 
+//----------------------------------------------------------------------------------------------------------------------
+// NeighborSolicitMessage
+
+NeighborSolicitMessage::NeighborSolicitMessage(void)
+{
+    OT_UNUSED_VARIABLE(mChecksum);
+    OT_UNUSED_VARIABLE(mReserved);
+
+    Clear();
+    mType = Icmp::Header::kTypeNeighborSolicit;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// NeighborAdvertMessage
+
+NeighborAdvertMessage::NeighborAdvertMessage(void)
+{
+    OT_UNUSED_VARIABLE(mChecksum);
+    OT_UNUSED_VARIABLE(mReserved);
+
+    Clear();
+    mType = Icmp::Header::kTypeNeighborAdvert;
+}
+
 } // namespace Nd
 } // namespace Ip6
 } // namespace ot
diff --git a/src/core/net/nd6.hpp b/src/core/net/nd6.hpp
index 0ec6146..8a084a9 100644
--- a/src/core/net/nd6.hpp
+++ b/src/core/net/nd6.hpp
@@ -156,7 +156,7 @@
     private:
         static const Option *Next(const Option *aOption);
         void                 Advance(void);
-        const Option *       Validate(const Option *aOption) const;
+        const Option        *Validate(const Option *aOption) const;
 
         const Option *mOption;
         const Option *mEnd;
@@ -175,6 +175,8 @@
 OT_TOOL_PACKED_BEGIN
 class PrefixInfoOption : public Option, private Clearable<PrefixInfoOption>
 {
+    friend class Clearable<PrefixInfoOption>;
+
 public:
     static constexpr Type kType = kTypePrefixInfo; ///< Prefix Information Option Type.
 
@@ -330,6 +332,8 @@
 OT_TOOL_PACKED_BEGIN
 class RouteInfoOption : public Option, private Clearable<RouteInfoOption>
 {
+    friend class Clearable<RouteInfoOption>;
+
 public:
     static constexpr uint16_t kMinSize = kLengthUnit;    ///< Minimum size (in bytes) of a Route Info Option
     static constexpr Type     kType    = kTypeRouteInfo; ///< Route Information Option Type.
@@ -442,7 +446,7 @@
     static constexpr uint8_t kPreferenceOffset = 3;
     static constexpr uint8_t kPreferenceMask   = 3 << kPreferenceOffset;
 
-    uint8_t *      GetPrefixBytes(void) { return AsNonConst(AsConst(this)->GetPrefixBytes()); }
+    uint8_t       *GetPrefixBytes(void) { return AsNonConst(AsConst(this)->GetPrefixBytes()); }
     const uint8_t *GetPrefixBytes(void) const { return reinterpret_cast<const uint8_t *>(this) + sizeof(*this); }
 
     uint8_t  mPrefixLength;  // The prefix length in bits.
@@ -470,6 +474,8 @@
     OT_TOOL_PACKED_BEGIN
     class Header : public Equatable<Header>, private Clearable<Header>
     {
+        friend class Clearable<Header>;
+
     public:
         /**
          * This constructor initializes the Router Advertisement message with
@@ -663,7 +669,7 @@
 private:
     const uint8_t *GetOptionStart(void) const { return (mData.GetBytes() + sizeof(Header)); }
     const uint8_t *GetDataEnd(void) const { return mData.GetBytes() + mData.GetLength(); }
-    Option *       AppendOption(uint16_t aOptionSize);
+    Option        *AppendOption(uint16_t aOptionSize);
 
     Data<kWithUint16Length> mData;
     uint16_t                mMaxLength;
@@ -692,6 +698,190 @@
 
 static_assert(sizeof(RouterSolicitMessage) == 8, "invalid RouterSolicitMessage structure");
 
+/**
+ * This class represents a Neighbor Solicitation (NS) message.
+ *
+ */
+OT_TOOL_PACKED_BEGIN
+class NeighborSolicitMessage : public Clearable<NeighborSolicitMessage>
+{
+public:
+    /**
+     * This constructor initializes the Neighbor Solicitation message.
+     *
+     */
+    NeighborSolicitMessage(void);
+
+    /**
+     * This method indicates whether the Neighbor Solicitation message is valid (proper Type and Code).
+     *
+     * @retval TRUE  If the message is valid.
+     * @retval FALSE If the message is not valid.
+     *
+     */
+    bool IsValid(void) const { return (mType == Icmp::Header::kTypeNeighborSolicit) && (mCode == 0); }
+
+    /**
+     * This method gets the Target Address field.
+     *
+     * @returns The Target Address.
+     *
+     */
+    const Address &GetTargetAddress(void) const { return mTargetAddress; }
+
+    /**
+     * This method sets the Target Address field.
+     *
+     * @param[in] aTargetAddress  The Target Address.
+     *
+     */
+    void SetTargetAddress(const Address &aTargetAddress) { mTargetAddress = aTargetAddress; }
+
+private:
+    // Neighbor Solicitation Message (RFC 4861)
+    //
+    //   0                   1                   2                   3
+    //   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |     Type      |     Code      |          Checksum             |
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |                           Reserved                            |
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |                                                               |
+    //  +                                                               +
+    //  |                                                               |
+    //  +                       Target Address                          +
+    //  |                                                               |
+    //  +                                                               +
+    //  |                                                               |
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |   Options ...
+    //  +-+-+-+-+-+-+-+-+-+-+-+-
+
+    uint8_t  mType;
+    uint8_t  mCode;
+    uint16_t mChecksum;
+    uint32_t mReserved;
+    Address  mTargetAddress;
+} OT_TOOL_PACKED_END;
+
+static_assert(sizeof(NeighborSolicitMessage) == 24, "Invalid NeighborSolicitMessage definition");
+
+/**
+ * This class represents a Neighbor Advertisement (NA) message.
+ *
+ */
+OT_TOOL_PACKED_BEGIN
+class NeighborAdvertMessage : public Clearable<NeighborAdvertMessage>
+{
+public:
+    NeighborAdvertMessage(void);
+
+    /**
+     * This method indicates whether the Neighbor Advertisement message is valid (proper Type and Code).
+     *
+     * @retval TRUE  If the message is valid.
+     * @retval FALSE If the message is not valid.
+     *
+     */
+    bool IsValid(void) const { return (mType == Icmp::Header::kTypeNeighborAdvert) && (mCode == 0); }
+
+    /**
+     * This method indicates whether or not the Router Flag is set in the NA message.
+     *
+     * @retval TRUE   The Router Flag is set.
+     * @retval FALSE  The Router Flag is not set.
+     *
+     */
+    bool IsRouterFlagSet(void) const { return (mFlags & kRouterFlag) != 0; }
+
+    /**
+     * This method sets the Router Flag in the NA message.
+     *
+     */
+    void SetRouterFlag(void) { mFlags |= kRouterFlag; }
+
+    /**
+     * This method indicates whether or not the Solicited Flag is set in the NA message.
+     *
+     * @retval TRUE   The Solicited Flag is set.
+     * @retval FALSE  The Solicited Flag is not set.
+     *
+     */
+    bool IsSolicitedFlagSet(void) const { return (mFlags & kSolicitedFlag) != 0; }
+
+    /**
+     * This method sets the Solicited Flag in the NA message.
+     *
+     */
+    void SetSolicitedFlag(void) { mFlags |= kSolicitedFlag; }
+
+    /**
+     * This method indicates whether or not the Override Flag is set in the NA message.
+     *
+     * @retval TRUE   The Override Flag is set.
+     * @retval FALSE  The Override Flag is not set.
+     *
+     */
+    bool IsOverrideFlagSet(void) const { return (mFlags & kOverrideFlag) != 0; }
+
+    /**
+     * This method sets the Override Flag in the NA message.
+     *
+     */
+    void SetOverrideFlag(void) { mFlags |= kOverrideFlag; }
+
+    /**
+     * This method gets the Target Address field.
+     *
+     * @returns The Target Address.
+     *
+     */
+    const Address &GetTargetAddress(void) const { return mTargetAddress; }
+
+    /**
+     * This method sets the Target Address field.
+     *
+     * @param[in] aTargetAddress  The Target Address.
+     *
+     */
+    void SetTargetAddress(const Address &aTargetAddress) { mTargetAddress = aTargetAddress; }
+
+private:
+    // Neighbor Advertisement Message (RFC 4861)
+    //
+    //   0                   1                   2                   3
+    //   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |     Type      |     Code      |          Checksum             |
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |R|S|O|                     Reserved                            |
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |                                                               |
+    //  +                                                               +
+    //  |                                                               |
+    //  +                       Target Address                          +
+    //  |                                                               |
+    //  +                                                               +
+    //  |                                                               |
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  |   Options ...
+    //  +-+-+-+-+-+-+-+-+-+-+-+-
+
+    static constexpr uint8_t kRouterFlag    = (1 << 7);
+    static constexpr uint8_t kSolicitedFlag = (1 << 6);
+    static constexpr uint8_t kOverrideFlag  = (1 << 5);
+
+    uint8_t  mType;
+    uint8_t  mCode;
+    uint16_t mChecksum;
+    uint8_t  mFlags;
+    uint8_t  mReserved[3];
+    Address  mTargetAddress;
+} OT_TOOL_PACKED_END;
+
+static_assert(sizeof(NeighborAdvertMessage) == 24, "Invalid NeighborAdvertMessage definition");
+
 } // namespace Nd
 } // namespace Ip6
 } // namespace ot
diff --git a/src/core/net/netif.cpp b/src/core/net/netif.cpp
index a55914f..009feaa 100644
--- a/src/core/net/netif.cpp
+++ b/src/core/net/netif.cpp
@@ -110,8 +110,6 @@
 Netif::Netif(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mMulticastPromiscuous(false)
-    , mAddressCallback(nullptr)
-    , mAddressCallbackContext(nullptr)
 {
 }
 
@@ -143,25 +141,7 @@
         tail->SetNext(&linkLocalAllNodesAddress);
     }
 
-    Get<Notifier>().Signal(kEventIp6MulticastSubscribed);
-
-#if !OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-    VerifyOrExit(mAddressCallback != nullptr);
-#endif
-
-    for (const MulticastAddress *entry = &linkLocalAllNodesAddress; entry; entry = entry->GetNext())
-    {
-#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-        Get<Utils::HistoryTracker>().RecordAddressEvent(kAddressAdded, *entry, kOriginThread);
-
-        if (mAddressCallback != nullptr)
-#endif
-        {
-            AddressInfo addressInfo(*entry);
-
-            mAddressCallback(&addressInfo, kAddressAdded, mAddressCallbackContext);
-        }
-    }
+    SignalMulticastAddressChange(kAddressAdded, &linkLocalAllNodesAddress, nullptr);
 
 exit:
     return;
@@ -169,7 +149,7 @@
 
 void Netif::UnsubscribeAllNodesMulticast(void)
 {
-    MulticastAddress *      prev;
+    MulticastAddress       *prev;
     const MulticastAddress &linkLocalAllNodesAddress = AsCoreType(&AsNonConst(kLinkLocalAllNodesMulticastAddress));
 
     // The tail of multicast address linked list contains the
@@ -199,25 +179,7 @@
         prev->SetNext(nullptr);
     }
 
-    Get<Notifier>().Signal(kEventIp6MulticastUnsubscribed);
-
-#if !OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-    VerifyOrExit(mAddressCallback != nullptr);
-#endif
-
-    for (const MulticastAddress *entry = &linkLocalAllNodesAddress; entry; entry = entry->GetNext())
-    {
-#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-        Get<Utils::HistoryTracker>().RecordAddressEvent(kAddressRemoved, *entry, kOriginThread);
-
-        if (mAddressCallback != nullptr)
-#endif
-        {
-            AddressInfo addressInfo(*entry);
-
-            mAddressCallback(&addressInfo, kAddressRemoved, mAddressCallbackContext);
-        }
-    }
+    SignalMulticastAddressChange(kAddressRemoved, &linkLocalAllNodesAddress, nullptr);
 
 exit:
     return;
@@ -260,26 +222,7 @@
         prev->SetNext(&linkLocalAllRoutersAddress);
     }
 
-    Get<Notifier>().Signal(kEventIp6MulticastSubscribed);
-
-#if !OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-    VerifyOrExit(mAddressCallback != nullptr);
-#endif
-
-    for (const MulticastAddress *entry = &linkLocalAllRoutersAddress; entry != &linkLocalAllNodesAddress;
-         entry                         = entry->GetNext())
-    {
-#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-        Get<Utils::HistoryTracker>().RecordAddressEvent(kAddressAdded, *entry, kOriginThread);
-
-        if (mAddressCallback != nullptr)
-#endif
-        {
-            AddressInfo addressInfo(*entry);
-
-            mAddressCallback(&addressInfo, kAddressAdded, mAddressCallbackContext);
-        }
-    }
+    SignalMulticastAddressChange(kAddressAdded, &linkLocalAllRoutersAddress, &linkLocalAllNodesAddress);
 
 exit:
     return;
@@ -312,27 +255,43 @@
         prev->SetNext(&linkLocalAllNodesAddress);
     }
 
-    Get<Notifier>().Signal(kEventIp6MulticastUnsubscribed);
+    SignalMulticastAddressChange(kAddressRemoved, &linkLocalAllRoutersAddress, &linkLocalAllNodesAddress);
+
+exit:
+    return;
+}
+
+void Netif::SignalMulticastAddressChange(AddressEvent            aAddressEvent,
+                                         const MulticastAddress *aStart,
+                                         const MulticastAddress *aEnd)
+{
+    // Signal changes to fixed multicast addresses from `aStart` up to
+    // (not including) `aEnd`. `aAddressEvent` indicates whether
+    // addresses were subscribed or unsubscribed.
+
+    Get<Notifier>().Signal(aAddressEvent == kAddressAdded ? kEventIp6MulticastSubscribed
+                                                          : kEventIp6MulticastUnsubscribed);
 
 #if !OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-    VerifyOrExit(mAddressCallback != nullptr);
+    VerifyOrExit(mAddressCallback.IsSet());
 #endif
 
-    for (const MulticastAddress *entry = &linkLocalAllRoutersAddress; entry != &linkLocalAllNodesAddress;
-         entry                         = entry->GetNext())
+    for (const MulticastAddress *entry = aStart; entry != aEnd; entry = entry->GetNext())
     {
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
-        Get<Utils::HistoryTracker>().RecordAddressEvent(kAddressRemoved, *entry, kOriginThread);
+        Get<Utils::HistoryTracker>().RecordAddressEvent(aAddressEvent, *entry, kOriginThread);
 
-        if (mAddressCallback != nullptr)
+        if (mAddressCallback.IsSet())
 #endif
         {
             AddressInfo addressInfo(*entry);
 
-            mAddressCallback(&addressInfo, kAddressRemoved, mAddressCallbackContext);
+            mAddressCallback.Invoke(&addressInfo, aAddressEvent);
         }
     }
 
+    ExitNow();
+
 exit:
     return;
 }
@@ -352,11 +311,11 @@
     Get<Utils::HistoryTracker>().RecordAddressEvent(kAddressAdded, aAddress, kOriginThread);
 #endif
 
-    if (mAddressCallback != nullptr)
+    if (mAddressCallback.IsSet())
     {
         AddressInfo addressInfo(aAddress);
 
-        mAddressCallback(&addressInfo, kAddressAdded, mAddressCallbackContext);
+        mAddressCallback.Invoke(&addressInfo, kAddressAdded);
     }
 
 exit:
@@ -373,11 +332,11 @@
     Get<Utils::HistoryTracker>().RecordAddressEvent(kAddressRemoved, aAddress, kOriginThread);
 #endif
 
-    if (mAddressCallback != nullptr)
+    if (mAddressCallback.IsSet())
     {
         AddressInfo addressInfo(aAddress);
 
-        mAddressCallback(&addressInfo, kAddressRemoved, mAddressCallbackContext);
+        mAddressCallback.Invoke(&addressInfo, kAddressRemoved);
     }
 
 exit:
@@ -461,12 +420,6 @@
     }
 }
 
-void Netif::SetAddressCallback(otIp6AddressCallback aCallback, void *aCallbackContext)
-{
-    mAddressCallback        = aCallback;
-    mAddressCallbackContext = aCallbackContext;
-}
-
 void Netif::AddUnicastAddress(UnicastAddress &aAddress)
 {
     SuccessOrExit(mUnicastAddresses.Add(aAddress));
@@ -477,11 +430,11 @@
     Get<Utils::HistoryTracker>().RecordAddressEvent(kAddressAdded, aAddress);
 #endif
 
-    if (mAddressCallback != nullptr)
+    if (mAddressCallback.IsSet())
     {
         AddressInfo addressInfo(aAddress);
 
-        mAddressCallback(&addressInfo, kAddressAdded, mAddressCallbackContext);
+        mAddressCallback.Invoke(&addressInfo, kAddressAdded);
     }
 
 exit:
@@ -498,11 +451,11 @@
     Get<Utils::HistoryTracker>().RecordAddressEvent(kAddressRemoved, aAddress);
 #endif
 
-    if (mAddressCallback != nullptr)
+    if (mAddressCallback.IsSet())
     {
         AddressInfo addressInfo(aAddress);
 
-        mAddressCallback(&addressInfo, kAddressRemoved, mAddressCallbackContext);
+        mAddressCallback.Invoke(&addressInfo, kAddressRemoved);
     }
 
 exit:
@@ -586,10 +539,7 @@
     }
 }
 
-bool Netif::HasUnicastAddress(const Address &aAddress) const
-{
-    return mUnicastAddresses.ContainsMatching(aAddress);
-}
+bool Netif::HasUnicastAddress(const Address &aAddress) const { return mUnicastAddresses.ContainsMatching(aAddress); }
 
 bool Netif::IsUnicastAddressExternal(const UnicastAddress &aAddress) const
 {
diff --git a/src/core/net/netif.hpp b/src/core/net/netif.hpp
index 6501355..29f3b61 100644
--- a/src/core/net/netif.hpp
+++ b/src/core/net/netif.hpp
@@ -37,6 +37,7 @@
 #include "openthread-core-config.h"
 
 #include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/clearable.hpp"
 #include "common/code_utils.hpp"
 #include "common/const_cast.hpp"
@@ -305,7 +306,7 @@
                 Iterator end(void) { return Iterator(mNetif, Iterator::kEndIterator); }
 
             private:
-                const Netif &       mNetif;
+                const Netif        &mNetif;
                 Address::TypeFilter mFilter;
             };
 
@@ -322,7 +323,7 @@
             void AdvanceFrom(const MulticastAddress *aAddr);
             void Advance(void) { AdvanceFrom(mItem->GetNext()); }
 
-            const Netif &       mNetif;
+            const Netif        &mNetif;
             Address::TypeFilter mFilter;
         };
 
@@ -367,7 +368,10 @@
      * @param[in]  aCallbackContext  A pointer to application-specific context.
      *
      */
-    void SetAddressCallback(otIp6AddressCallback aCallback, void *aCallbackContext);
+    void SetAddressCallback(otIp6AddressCallback aCallback, void *aCallbackContext)
+    {
+        mAddressCallback.Set(aCallback, aCallbackContext);
+    }
 
     /**
      * This method returns the linked list of unicast addresses.
@@ -644,12 +648,15 @@
     void UnsubscribeAllNodesMulticast(void);
 
 private:
+    void SignalMulticastAddressChange(AddressEvent            aAddressEvent,
+                                      const MulticastAddress *aStart,
+                                      const MulticastAddress *aEnd);
+
     LinkedList<UnicastAddress>   mUnicastAddresses;
     LinkedList<MulticastAddress> mMulticastAddresses;
     bool                         mMulticastPromiscuous;
 
-    otIp6AddressCallback mAddressCallback;
-    void *               mAddressCallbackContext;
+    Callback<otIp6AddressCallback> mAddressCallback;
 
     Pool<UnicastAddress, OPENTHREAD_CONFIG_IP6_MAX_EXT_UCAST_ADDRS>           mExtUnicastAddressPool;
     Pool<ExternalMulticastAddress, OPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS> mExtMulticastAddressPool;
diff --git a/src/core/net/sntp_client.cpp b/src/core/net/sntp_client.cpp
index b2e82f2..e93effa 100644
--- a/src/core/net/sntp_client.cpp
+++ b/src/core/net/sntp_client.cpp
@@ -95,7 +95,7 @@
 
 Client::Client(Instance &aInstance)
     : mSocket(aInstance)
-    , mRetransmissionTimer(aInstance, Client::HandleRetransmissionTimer)
+    , mRetransmissionTimer(aInstance)
     , mUnixEra(0)
 {
 }
@@ -105,7 +105,7 @@
     Error error;
 
     SuccessOrExit(error = mSocket.Open(&Client::HandleUdpReceive, this));
-    SuccessOrExit(error = mSocket.Bind(0, OT_NETIF_UNSPECIFIED));
+    SuccessOrExit(error = mSocket.Bind(0, Ip6::kNetifUnspecified));
 
 exit:
     return error;
@@ -128,8 +128,8 @@
 {
     Error                   error;
     QueryMetadata           queryMetadata(aHandler, aContext);
-    Message *               message     = nullptr;
-    Message *               messageCopy = nullptr;
+    Message                *message     = nullptr;
+    Message                *messageCopy = nullptr;
     Header                  header;
     const Ip6::MessageInfo *messageInfo;
 
@@ -256,7 +256,7 @@
     return matchedMessage;
 }
 
-void Client::FinalizeSntpTransaction(Message &            aQuery,
+void Client::FinalizeSntpTransaction(Message             &aQuery,
                                      const QueryMetadata &aQueryMetadata,
                                      uint64_t             aTime,
                                      Error                aResult)
@@ -269,11 +269,6 @@
     }
 }
 
-void Client::HandleRetransmissionTimer(Timer &aTimer)
-{
-    aTimer.Get<Client>().HandleRetransmissionTimer();
-}
-
 void Client::HandleRetransmissionTimer(void)
 {
     TimeMilli        now      = TimerMilli::GetNow();
@@ -307,10 +302,7 @@
             SendCopy(message, messageInfo);
         }
 
-        if (nextTime > queryMetadata.mTransmissionTime)
-        {
-            nextTime = queryMetadata.mTransmissionTime;
-        }
+        nextTime = Min(nextTime, queryMetadata.mTransmissionTime);
     }
 
     if (nextTime < now.GetDistantFuture())
@@ -331,7 +323,7 @@
     Error         error = kErrorNone;
     Header        responseHeader;
     QueryMetadata queryMetadata;
-    Message *     message  = nullptr;
+    Message      *message  = nullptr;
     uint64_t      unixTime = 0;
 
     SuccessOrExit(aMessage.Read(aMessage.GetOffset(), responseHeader));
diff --git a/src/core/net/sntp_client.hpp b/src/core/net/sntp_client.hpp
index 1a9ec4f..f8338d5 100644
--- a/src/core/net/sntp_client.hpp
+++ b/src/core/net/sntp_client.hpp
@@ -440,7 +440,7 @@
 private:
     uint32_t              mTransmitTimestamp;   ///< Time at the client when the request departed for the server.
     otSntpResponseHandler mResponseHandler;     ///< A function pointer that is called on response reception.
-    void *                mResponseContext;     ///< A pointer to arbitrary context information.
+    void                 *mResponseContext;     ///< A pointer to arbitrary context information.
     TimeMilli             mTransmissionTime;    ///< Time when the timer should shoot for this message.
     Ip6::Address          mSourceAddress;       ///< IPv6 address of the message source.
     Ip6::Address          mDestinationAddress;  ///< IPv6 address of the message destination.
@@ -524,16 +524,17 @@
     Message *FindRelatedQuery(const Header &aResponseHeader, QueryMetadata &aQueryMetadata);
     void FinalizeSntpTransaction(Message &aQuery, const QueryMetadata &aQueryMetadata, uint64_t aTime, Error aResult);
 
-    static void HandleRetransmissionTimer(Timer &aTimer);
-    void        HandleRetransmissionTimer(void);
+    void HandleRetransmissionTimer(void);
 
     static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
     void        HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
+    using RetxTimer = TimerMilliIn<Client, &Client::HandleRetransmissionTimer>;
+
     Ip6::Udp::Socket mSocket;
 
     MessageQueue mPendingQueries;
-    TimerMilli   mRetransmissionTimer;
+    RetxTimer    mRetransmissionTimer;
 
     uint32_t mUnixEra;
 };
diff --git a/src/core/net/srp_client.cpp b/src/core/net/srp_client.cpp
index 5bf507b..d964731 100644
--- a/src/core/net/srp_client.cpp
+++ b/src/core/net/srp_client.cpp
@@ -35,6 +35,7 @@
 #include "common/debug.hpp"
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 #include "common/settings.hpp"
 #include "common/string.hpp"
@@ -82,7 +83,7 @@
     mNumAddresses = 0;
     mAutoAddress  = true;
 
-    LogInfo("HostInfo enabled auto address", GetNumAddresses());
+    LogInfo("HostInfo enabled auto address");
 }
 
 void Client::HostInfo::SetAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses)
@@ -113,6 +114,9 @@
     // to avoid logging.
     mState = OT_SRP_CLIENT_ITEM_STATE_REMOVED;
 
+    mLease    = Min(mLease, kMaxLease);
+    mKeyLease = Min(mKeyLease, kMaxLease);
+
 exit:
     return error;
 }
@@ -202,18 +206,9 @@
     }
 }
 
-void Client::AutoStart::SetCallback(AutoStartCallback aCallback, void *aContext)
-{
-    mCallback = aCallback;
-    mContext  = aContext;
-}
-
 void Client::AutoStart::InvokeCallback(const Ip6::SockAddr *aServerSockAddr) const
 {
-    if (mCallback != nullptr)
-    {
-        mCallback(aServerSockAddr, mContext);
-    }
+    mCallback.InvokeIfSet(aServerSockAddr);
 }
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
@@ -250,20 +245,21 @@
     , mTxFailureRetryCount(0)
     , mShouldRemoveKeyLease(false)
     , mAutoHostAddressAddedMeshLocal(false)
+    , mSingleServiceMode(false)
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     , mServiceKeyRecordEnabled(false)
+    , mUseShortLeaseOption(false)
 #endif
     , mUpdateMessageId(0)
     , mRetryWaitInterval(kMinRetryWaitInterval)
-    , mAcceptedLeaseInterval(0)
     , mTtl(0)
-    , mLeaseInterval(kDefaultLease)
-    , mKeyLeaseInterval(kDefaultKeyLease)
+    , mLease(0)
+    , mKeyLease(0)
+    , mDefaultLease(kDefaultLease)
+    , mDefaultKeyLease(kDefaultKeyLease)
     , mSocket(aInstance)
-    , mCallback(nullptr)
-    , mCallbackContext(nullptr)
     , mDomainName(kDefaultDomainName)
-    , mTimer(aInstance, Client::HandleTimer)
+    , mTimer(aInstance)
 {
     mHostInfo.Init();
 
@@ -335,7 +331,7 @@
 
     VerifyOrExit(GetState() != kStateStopped);
 
-    mSingleServiceMode.Disable();
+    mSingleServiceMode = false;
 
     // State changes:
     //   kAdding     -> kToRefresh
@@ -343,7 +339,7 @@
     //   kRemoving   -> kToRemove
     //   kRegistered -> kToRefresh
 
-    ChangeHostAndServiceStates(kNewStateOnStop);
+    ChangeHostAndServiceStates(kNewStateOnStop, kForAllServices);
 
     IgnoreError(mSocket.Close());
 
@@ -359,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)
     {
@@ -376,12 +372,6 @@
 #endif
 }
 
-void Client::SetCallback(Callback aCallback, void *aContext)
-{
-    mCallback        = aCallback;
-    mCallbackContext = aContext;
-}
-
 void Client::Resume(void)
 {
     SetState(kStateUpdated);
@@ -405,14 +395,14 @@
         /* (7) kRemoved    -> */ kRemoved,
     };
 
-    mSingleServiceMode.Disable();
+    mSingleServiceMode = false;
 
     // State changes:
     //   kAdding     -> kToRefresh
     //   kRefreshing -> kToRefresh
     //   kRemoving   -> kToRemove
 
-    ChangeHostAndServiceStates(kNewStateOnPause);
+    ChangeHostAndServiceStates(kNewStateOnPause, kForAllServices);
 
     SetState(kStatePaused);
 }
@@ -569,16 +559,6 @@
     VerifyOrExit(mServices.Contains(aService), error = kErrorNotFound);
 
     UpdateServiceStateToRemove(aService);
-
-    // Check if the service was removed immediately, if so
-    // invoke the callback to report the removed service.
-    GetRemovedServices(removedServices);
-
-    if (!removedServices.IsEmpty())
-    {
-        InvokeCallback(kErrorNone, mHostInfo, removedServices.GetHead());
-    }
-
     UpdateState();
 
 exit:
@@ -587,12 +567,7 @@
 
 void Client::UpdateServiceStateToRemove(Service &aService)
 {
-    if (aService.GetState() == kToAdd)
-    {
-        // If the service has not been added yet, we can remove it immediately.
-        aService.SetState(kRemoved);
-    }
-    else if (aService.GetState() != kRemoving)
+    if (aService.GetState() != kRemoving)
     {
         aService.SetState(kToRemove);
     }
@@ -704,7 +679,7 @@
     return;
 }
 
-void Client::ChangeHostAndServiceStates(const ItemState *aNewStates)
+void Client::ChangeHostAndServiceStates(const ItemState *aNewStates, ServiceStateChangeMode aMode)
 {
 #if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
     ItemState oldHostState = mHostInfo.GetState();
@@ -714,7 +689,7 @@
 
     for (Service &service : mServices)
     {
-        if (mSingleServiceMode.IsEnabled() && mSingleServiceMode.GetService() != &service)
+        if ((aMode == kForServicesAppendedInMessage) && !service.IsAppendedInMessage())
         {
             continue;
         }
@@ -748,18 +723,11 @@
 #endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
 }
 
-void Client::InvokeCallback(Error aError) const
-{
-    InvokeCallback(aError, mHostInfo, nullptr);
-}
+void Client::InvokeCallback(Error aError) const { InvokeCallback(aError, mHostInfo, nullptr); }
 
 void Client::InvokeCallback(Error aError, const HostInfo &aHostInfo, const Service *aRemovedServices) const
 {
-    VerifyOrExit(mCallback != nullptr);
-    mCallback(aError, &aHostInfo, mServices.GetHead(), aRemovedServices, mCallbackContext);
-
-exit:
-    return;
+    mCallback.InvokeIfSet(aError, &aHostInfo, mServices.GetHead(), aRemovedServices);
 }
 
 void Client::SendUpdate(void)
@@ -776,7 +744,7 @@
     };
 
     Error    error   = kErrorNone;
-    Message *message = mSocket.NewMessage(0);
+    Message *message = mSocket.NewMessage();
     uint32_t length;
 
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
@@ -786,8 +754,8 @@
 
     if (length >= Ip6::kMaxDatagramLength)
     {
-        LogInfo("Msg len %u is larger than MTU, enabling single service mode", length);
-        mSingleServiceMode.Enable();
+        LogInfo("Msg len %lu is larger than MTU, enabling single service mode", ToUlong(length));
+        mSingleServiceMode = true;
         IgnoreError(message->SetLength(0));
         SuccessOrExit(error = PrepareUpdateMessage(*message));
     }
@@ -801,7 +769,7 @@
     //   kToRefresh -> kRefreshing
     //   kToRemove  -> kRemoving
 
-    ChangeHostAndServiceStates(kNewStateOnMessageTx);
+    ChangeHostAndServiceStates(kNewStateOnMessageTx, kForServicesAppendedInMessage);
 
     // Remember the update message tx time to use later to determine the
     // lease renew time.
@@ -829,7 +797,7 @@
 
         LogInfo("Failed to send update: %s", ErrorToString(error));
 
-        mSingleServiceMode.Disable();
+        mSingleServiceMode = false;
         FreeMessage(message);
 
         SetState(kStateToRetry);
@@ -842,7 +810,7 @@
             interval = Random::NonCrypto::AddJitter(kTxFailureRetryInterval, kTxFailureRetryJitter);
             mTimer.Start(interval);
 
-            LogInfo("Quick retry %d in %u msec", mTxFailureRetryCount, interval);
+            LogInfo("Quick retry %u in %lu msec", mTxFailureRetryCount, ToUlong(interval));
 
             // Do not report message preparation errors to user
             // until `kMaxTxFailureRetries` are exhausted.
@@ -867,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
@@ -899,19 +872,7 @@
 
     // Prepare Update section
 
-    if ((mHostInfo.GetState() != kToRemove) && (mHostInfo.GetState() != kRemoving))
-    {
-        for (Service &service : mServices)
-        {
-            SuccessOrExit(error = AppendServiceInstructions(service, aMessage, info));
-
-            if (mSingleServiceMode.IsEnabled() && (mSingleServiceMode.GetService() != nullptr))
-            {
-                break;
-            }
-        }
-    }
-
+    SuccessOrExit(error = AppendServiceInstructions(aMessage, info));
     SuccessOrExit(error = AppendHostDescriptionInstruction(aMessage, info));
 
     header.SetUpdateRecordCount(info.mRecordCount);
@@ -929,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;
@@ -951,29 +940,181 @@
 exit:
     return error;
 }
+#endif //  OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
 
-Error Client::AppendServiceInstructions(Service &aService, Message &aMessage, Info &aInfo)
+Error Client::AppendServiceInstructions(Message &aMessage, Info &aInfo)
 {
-    Error               error = kErrorNone;
+    Error error = kErrorNone;
+
+    if ((mHostInfo.GetState() == kToRemove) || (mHostInfo.GetState() == kRemoving))
+    {
+        // When host is being removed, there is no need to include
+        // services in the message (server is expected to remove any
+        // previously registered services by this client). However, we
+        // still mark all services as if they are appended in the message
+        // so to ensure to update their state after sending the message.
+
+        for (Service &service : mServices)
+        {
+            service.MarkAsAppendedInMessage();
+        }
+
+        mLease    = 0;
+        mKeyLease = mShouldRemoveKeyLease ? 0 : mDefaultKeyLease;
+        ExitNow();
+    }
+
+    mLease    = kUnspecifiedInterval;
+    mKeyLease = kUnspecifiedInterval;
+
+    // We first go through all services which are being updated (in any
+    // of `...ing` states) and determine the lease and key lease intervals
+    // associated with them. By the end of the loop either of `mLease` or
+    // `mKeyLease` may be set or may still remain `kUnspecifiedInterval`.
+
+    for (Service &service : mServices)
+    {
+        uint32_t lease    = DetermineLeaseInterval(service.GetLease(), mDefaultLease);
+        uint32_t keyLease = Max(DetermineLeaseInterval(service.GetKeyLease(), mDefaultKeyLease), lease);
+
+        service.ClearAppendedInMessageFlag();
+
+        switch (service.GetState())
+        {
+        case kAdding:
+        case kRefreshing:
+            OT_ASSERT((mLease == kUnspecifiedInterval) || (mLease == lease));
+            mLease = lease;
+
+            OT_FALL_THROUGH;
+
+        case kRemoving:
+            OT_ASSERT((mKeyLease == kUnspecifiedInterval) || (mKeyLease == keyLease));
+            mKeyLease = keyLease;
+            break;
+
+        case kToAdd:
+        case kToRefresh:
+        case kToRemove:
+        case kRegistered:
+        case kRemoved:
+            break;
+        }
+    }
+
+    // We go through all services again and append the services that
+    // match the selected `mLease` and `mKeyLease`. If the lease intervals
+    // are not yet set, the first appended service will determine them.
+
+    for (Service &service : mServices)
+    {
+        // Skip over services that are already registered in this loop.
+        // They may be added from the loop below once the lease intervals
+        // are determined.
+
+        if ((service.GetState() != kRegistered) && CanAppendService(service))
+        {
+            SuccessOrExit(error = AppendServiceInstruction(service, aMessage, aInfo));
+
+            if (mSingleServiceMode)
+            {
+                // In "single service mode", we allow only one service
+                // to be appended in the message.
+                break;
+            }
+        }
+    }
+
+    if (!mSingleServiceMode)
+    {
+        for (Service &service : mServices)
+        {
+            if ((service.GetState() == kRegistered) && CanAppendService(service) && ShouldRenewEarly(service))
+            {
+                // If the lease needs to be renewed or if we are close to the
+                // renewal time of a registered service, we refresh the service
+                // early and include it in this update. This helps put more
+                // services on the same lease refresh schedule.
+
+                service.SetState(kToRefresh);
+                SuccessOrExit(error = AppendServiceInstruction(service, aMessage, aInfo));
+            }
+        }
+    }
+
+    // `mLease` or `mKeylease` may be determined from the set of
+    // services included in the message. If they are not yet set we
+    // use the default intervals.
+
+    mLease    = DetermineLeaseInterval(mLease, mDefaultLease);
+    mKeyLease = DetermineLeaseInterval(mKeyLease, mDefaultKeyLease);
+
+    // When message only contains removal of a previously registered
+    // service, then `mKeyLease` is set but `mLease` remains unspecified.
+    // In such a case, we end up using `mDefaultLease` but then we need
+    // to make sure it is not greater than the selected `mKeyLease`.
+
+    if (mLease > mKeyLease)
+    {
+        mLease = mKeyLease;
+    }
+
+exit:
+    return error;
+}
+
+bool Client::CanAppendService(const Service &aService)
+{
+    // Check the lease intervals associated with `aService` to see if
+    // it can be included in this message. When removing a service,
+    // only key lease interval should match. In all other cases, both
+    // lease and key lease should match. The `mLease` and/or `mKeyLease`
+    // may be updated if they were unspecified.
+
+    bool     canAppend = false;
+    uint32_t lease     = DetermineLeaseInterval(aService.GetLease(), mDefaultLease);
+    uint32_t keyLease  = Max(DetermineLeaseInterval(aService.GetKeyLease(), mDefaultKeyLease), lease);
+
+    switch (aService.GetState())
+    {
+    case kToAdd:
+    case kAdding:
+    case kToRefresh:
+    case kRefreshing:
+    case kRegistered:
+        VerifyOrExit((mLease == kUnspecifiedInterval) || (mLease == lease));
+        VerifyOrExit((mKeyLease == kUnspecifiedInterval) || (mKeyLease == keyLease));
+        mLease    = lease;
+        mKeyLease = keyLease;
+        canAppend = true;
+        break;
+
+    case kToRemove:
+    case kRemoving:
+        VerifyOrExit((mKeyLease == kUnspecifiedInterval) || (mKeyLease == keyLease));
+        mKeyLease = keyLease;
+        canAppend = true;
+        break;
+
+    case kRemoved:
+        break;
+    }
+
+exit:
+    return canAppend;
+}
+
+Error Client::AppendServiceInstruction(Service &aService, Message &aMessage, Info &aInfo)
+{
+    Error               error    = kErrorNone;
+    bool                removing = ((aService.GetState() == kToRemove) || (aService.GetState() == kRemoving));
     Dns::ResourceRecord rr;
     Dns::SrvRecord      srv;
-    bool                removing;
     uint16_t            serviceNameOffset;
     uint16_t            instanceNameOffset;
     uint16_t            offset;
 
-    if (aService.GetState() == kRegistered)
-    {
-        // If the lease needs to be renewed or if we are close to the
-        // renewal time of a registered service, we refresh the service
-        // early and include it in this update. This helps put more
-        // services on the same lease refresh schedule.
-
-        VerifyOrExit(ShouldRenewEarly(aService));
-        aService.SetState(kToRefresh);
-    }
-
-    removing = ((aService.GetState() == kToRemove) || (aService.GetState() == kRemoving));
+    aService.MarkAsAppendedInMessage();
 
     //----------------------------------
     // Service Discovery Instruction
@@ -989,7 +1130,7 @@
     // to NONE and TTL to zero (RFC 2136 - section 2.5.4).
 
     rr.Init(Dns::ResourceRecord::kTypePtr, removing ? Dns::PtrRecord::kClassNone : Dns::PtrRecord::kClassInternet);
-    rr.SetTtl(removing ? 0 : GetTtl());
+    rr.SetTtl(removing ? 0 : DetermineTtl());
     offset = aMessage.GetLength();
     SuccessOrExit(error = aMessage.Append(rr));
 
@@ -1048,7 +1189,7 @@
 
     SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage));
     srv.Init();
-    srv.SetTtl(GetTtl());
+    srv.SetTtl(DetermineTtl());
     srv.SetPriority(aService.GetPriority());
     srv.SetWeight(aService.GetWeight());
     srv.SetPort(aService.GetPort());
@@ -1081,11 +1222,6 @@
     }
 #endif
 
-    if (mSingleServiceMode.IsEnabled())
-    {
-        mSingleServiceMode.SetService(aService);
-    }
-
 exit:
     return error;
 }
@@ -1153,7 +1289,7 @@
     Dns::ResourceRecord rr;
 
     rr.Init(Dns::ResourceRecord::kTypeAaaa);
-    rr.SetTtl(GetTtl());
+    rr.SetTtl(DetermineTtl());
     rr.SetLength(sizeof(Ip6::Address));
 
     SuccessOrExit(error = AppendHostName(aMessage, aInfo));
@@ -1172,14 +1308,18 @@
     Crypto::Ecdsa::P256::PublicKey publicKey;
 
     key.Init();
-    key.SetTtl(GetTtl());
+    key.SetTtl(DetermineTtl());
     key.SetFlags(Dns::KeyRecord::kAuthConfidPermitted, Dns::KeyRecord::kOwnerNonZone,
                  Dns::KeyRecord::kSignatoryFlagGeneral);
     key.SetProtocol(Dns::KeyRecord::kProtocolDnsSec);
     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++;
 
@@ -1231,11 +1371,12 @@
     return error;
 }
 
-Error Client::AppendUpdateLeaseOptRecord(Message &aMessage) const
+Error Client::AppendUpdateLeaseOptRecord(Message &aMessage)
 {
     Error            error;
     Dns::OptRecord   optRecord;
     Dns::LeaseOption leaseOption;
+    uint16_t         optionSize;
 
     // Append empty (root domain) as OPT RR name.
     SuccessOrExit(error = Dns::Name::AppendTerminator(aMessage));
@@ -1245,24 +1386,26 @@
     optRecord.Init();
     optRecord.SetUdpPayloadSize(kUdpPayloadSize);
     optRecord.SetDnsSecurityFlag();
-    optRecord.SetLength(sizeof(Dns::LeaseOption));
 
-    SuccessOrExit(error = aMessage.Append(optRecord));
-
-    leaseOption.Init();
-
-    if ((mHostInfo.GetState() == kToRemove) || (mHostInfo.GetState() == kRemoving))
+#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+    if (mUseShortLeaseOption)
     {
-        leaseOption.SetLeaseInterval(0);
-        leaseOption.SetKeyLeaseInterval(mShouldRemoveKeyLease ? 0 : mKeyLeaseInterval);
+        LogInfo("Test mode - appending short variant of Lease Option");
+        mKeyLease = mLease;
+        leaseOption.InitAsShortVariant(mLease);
     }
     else
+#endif
     {
-        leaseOption.SetLeaseInterval(mLeaseInterval);
-        leaseOption.SetKeyLeaseInterval(mKeyLeaseInterval);
+        leaseOption.InitAsLongVariant(mLease, mKeyLease);
     }
 
-    error = aMessage.Append(leaseOption);
+    optionSize = static_cast<uint16_t>(leaseOption.GetSize());
+
+    optRecord.SetLength(optionSize);
+
+    SuccessOrExit(error = aMessage.Append(optRecord));
+    error = aMessage.AppendBytes(&leaseOption, optionSize);
 
 exit:
     return error;
@@ -1313,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.
@@ -1389,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());
@@ -1457,7 +1604,6 @@
     // interval accepted by server. If not present, then use the
     // transmitted lease interval from the update request message.
 
-    mAcceptedLeaseInterval = mLeaseInterval;
     recordCount =
         header.GetPrerequisiteRecordCount() + header.GetUpdateRecordCount() + header.GetAdditionalRecordCount();
 
@@ -1482,13 +1628,13 @@
     // lease interval is too short (shorter than the guard time) we
     // just use half of the accepted lease interval.
 
-    if (mAcceptedLeaseInterval > kLeaseRenewGuardInterval)
+    if (mLease > kLeaseRenewGuardInterval)
     {
-        mLeaseRenewTime += Time::SecToMsec(mAcceptedLeaseInterval - kLeaseRenewGuardInterval);
+        mLeaseRenewTime += Time::SecToMsec(mLease - kLeaseRenewGuardInterval);
     }
     else
     {
-        mLeaseRenewTime += Time::SecToMsec(mAcceptedLeaseInterval) / 2;
+        mLeaseRenewTime += Time::SecToMsec(mLease) / 2;
     }
 
     for (Service &service : mServices)
@@ -1504,8 +1650,7 @@
     //   kRefreshing -> kRegistered
     //   kRemoving   -> kRemoved
 
-    ChangeHostAndServiceStates(kNewStateOnUpdateDone);
-    mSingleServiceMode.Disable();
+    ChangeHostAndServiceStates(kNewStateOnUpdateDone, kForServicesAppendedInMessage);
 
     HandleUpdateDone();
     UpdateState();
@@ -1561,40 +1706,27 @@
     // Read and process all options (in an OPT RR) from a message.
     // The `aOffset` points to beginning of record in `aMessage`.
 
-    Error    error = kErrorNone;
-    uint16_t len;
+    Error            error = kErrorNone;
+    Dns::LeaseOption leaseOption;
 
     IgnoreError(Dns::Name::ParseName(aMessage, aOffset));
     aOffset += sizeof(Dns::OptRecord);
 
-    len = aOptRecord.GetLength();
-
-    while (len > 0)
+    switch (error = leaseOption.ReadFrom(aMessage, aOffset, aOptRecord.GetLength()))
     {
-        Dns::LeaseOption leaseOption;
-        Dns::Option &    option = leaseOption;
-        uint16_t         size;
+    case kErrorNone:
+        mLease    = Min(leaseOption.GetLeaseInterval(), kMaxLease);
+        mKeyLease = Min(leaseOption.GetKeyLeaseInterval(), kMaxLease);
+        break;
 
-        SuccessOrExit(error = aMessage.Read(aOffset, option));
+    case kErrorNotFound:
+        // If server does not include a lease option in its response, it
+        // indicates that it accepted what we requested.
+        error = kErrorNone;
+        break;
 
-        VerifyOrExit(aOffset + option.GetSize() <= aMessage.GetLength(), error = kErrorParse);
-
-        if ((option.GetOptionCode() == Dns::Option::kUpdateLease) &&
-            (option.GetOptionLength() >= Dns::LeaseOption::kOptionLength))
-        {
-            SuccessOrExit(error = aMessage.Read(aOffset, leaseOption));
-
-            mAcceptedLeaseInterval = leaseOption.GetLeaseInterval();
-
-            if (mAcceptedLeaseInterval > kMaxLease)
-            {
-                mAcceptedLeaseInterval = kMaxLease;
-            }
-        }
-
-        size = static_cast<uint16_t>(option.GetSize());
-        aOffset += size;
-        len -= size;
+    default:
+        ExitNow();
     }
 
 exit:
@@ -1677,9 +1809,9 @@
                     service.SetState(kToRefresh);
                     shouldUpdate = true;
                 }
-                else if (service.GetLeaseRenewTime() < earliestRenewTime)
+                else
                 {
-                    earliestRenewTime = service.GetLeaseRenewTime();
+                    earliestRenewTime = Min(earliestRenewTime, service.GetLeaseRenewTime());
                 }
 
                 break;
@@ -1719,33 +1851,47 @@
     }
 }
 
-uint32_t Client::GetBoundedLeaseInterval(uint32_t aInterval, uint32_t aDefaultInterval) const
+uint32_t Client::DetermineLeaseInterval(uint32_t aInterval, uint32_t aDefaultInterval) const
 {
-    uint32_t boundedInterval = aDefaultInterval;
+    // Determine the lease or key lease interval.
+    //
+    // We use `aInterval` if it is non-zero, otherwise, use the
+    // `aDefaultInterval`. We also ensure that the returned value is
+    // never greater than `kMaxLease`. The `kMaxLease` is selected
+    // such the lease intervals in msec can still fit in a `uint32_t`
+    // `Time` variable (`kMaxLease` is ~ 24.8 days).
 
-    if (aInterval != 0)
-    {
-        boundedInterval = OT_MIN(aInterval, static_cast<uint32_t>(kMaxLease));
-    }
+    return Min(kMaxLease, (aInterval != kUnspecifiedInterval) ? aInterval : aDefaultInterval);
+}
 
-    return boundedInterval;
+uint32_t Client::DetermineTtl(void) const
+{
+    // Determine the TTL to use based on current `mLease`.
+    // If `mLease == 0`, it indicates we are removing host
+    // and so we use `mDefaultLease` instead.
+
+    uint32_t lease = (mLease == 0) ? mDefaultLease : mLease;
+
+    return (mTtl == kUnspecifiedInterval) ? lease : Min(mTtl, lease);
 }
 
 bool Client::ShouldRenewEarly(const Service &aService) const
 {
     // Check if we reached the service renew time or close to it. The
     // "early renew interval" is used to allow early refresh. It is
-    // calculated as a factor of the `mAcceptedLeaseInterval`. The
-    // "early lease renew factor" is given as a fraction (numerator and
-    // denominator). If the denominator is set to zero (i.e., factor is
-    // set to infinity), then service is always included in all SRP
+    // calculated as a factor of the service requested lease interval.
+    // The  "early lease renew factor" is given as a fraction (numerator
+    // and denominator). If the denominator is set to zero (i.e., factor
+    // is set to infinity), then service is always included in all SRP
     // update messages.
 
     bool shouldRenew;
 
 #if OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_DENOMINATOR != 0
-    uint32_t earlyRenewInterval =
-        Time::SecToMsec(mAcceptedLeaseInterval) / kEarlyLeaseRenewFactorDenominator * kEarlyLeaseRenewFactorNumerator;
+    uint32_t earlyRenewInterval;
+
+    earlyRenewInterval = Time::SecToMsec(DetermineLeaseInterval(aService.GetLease(), mDefaultLease));
+    earlyRenewInterval = earlyRenewInterval / kEarlyLeaseRenewFactorDenominator * kEarlyLeaseRenewFactorNumerator;
 
     shouldRenew = (aService.GetLeaseRenewTime() <= TimerMilli::GetNow() + earlyRenewInterval);
 #else
@@ -1756,11 +1902,6 @@
     return shouldRenew;
 }
 
-void Client::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Client>().HandleTimer();
-}
-
 void Client::HandleTimer(void)
 {
     switch (GetState())
@@ -1775,7 +1916,7 @@
         break;
 
     case kStateUpdating:
-        mSingleServiceMode.Disable();
+        mSingleServiceMode = false;
         LogRetryWaitInterval();
         LogInfo("Timed out, no response");
         GrowRetryWaitInterval();
@@ -1789,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);
         }
@@ -2101,7 +2242,7 @@
 
     uint32_t interval = GetRetryWaitInterval();
 
-    LogInfo("Retry interval %u %s", (interval < kLogInMsecLimit) ? interval : Time::MsecToSec(interval),
+    LogInfo("Retry interval %lu %s", ToUlong((interval < kLogInMsecLimit) ? interval : Time::MsecToSec(interval)),
             (interval < kLogInMsecLimit) ? "ms" : "sec");
 }
 
diff --git a/src/core/net/srp_client.hpp b/src/core/net/srp_client.hpp
index 46bdc33..992940c 100644
--- a/src/core/net/srp_client.hpp
+++ b/src/core/net/srp_client.hpp
@@ -36,6 +36,7 @@
 #include <openthread/srp_client.h>
 
 #include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/clearable.hpp"
 #include "common/linked_list.hpp"
 #include "common/locator.hpp"
@@ -97,7 +98,7 @@
      * Please see `otSrpClientCallback` for more details.
      *
      */
-    typedef otSrpClientCallback Callback;
+    typedef otSrpClientCallback ClientCallback;
 
     /**
      * This type represents an SRP client host info.
@@ -106,6 +107,7 @@
     class HostInfo : public otSrpClientHostInfo, private Clearable<HostInfo>
     {
         friend class Client;
+        friend class Clearable<HostInfo>;
 
     public:
         /**
@@ -268,17 +270,45 @@
         uint8_t GetNumTxtEntries(void) const { return mNumTxtEntries; }
 
         /**
-         * This method get the state of service.
+         * This method gets the state of service.
          *
          * @returns The service state.
          *
          */
         ItemState GetState(void) const { return static_cast<ItemState>(mState); }
 
+        /**
+         * This method gets the desired lease interval to request when registering this service.
+         *
+         * @returns The desired lease interval in sec. Zero indicates to use default.
+         *
+         */
+        uint32_t GetLease(void) const { return (mLease & kLeaseMask); }
+
+        /**
+         * This method gets the desired key lease interval to request when registering this service.
+         *
+         * @returns The desired lease interval in sec. Zero indicates to use default.
+         *
+         */
+        uint32_t GetKeyLease(void) const { return mKeyLease; }
+
     private:
+        // We use the high (MSB) bit of `mLease` as flag to indicate
+        // whether or not the service is appended in the message.
+        // This is then used when updating the service state. Note that
+        // we guarantee that `mLease` is not greater than `kMaxLease`
+        // which ensures that the last bit is unused.
+
+        static constexpr uint32_t kAppendedInMsgFlag = (1U << 31);
+        static constexpr uint32_t kLeaseMask         = ~kAppendedInMsgFlag;
+
         void      SetState(ItemState aState);
         TimeMilli GetLeaseRenewTime(void) const { return TimeMilli(mData); }
         void      SetLeaseRenewTime(TimeMilli aTime) { mData = aTime.GetValue(); }
+        bool      IsAppendedInMessage(void) const { return mLease & kAppendedInMsgFlag; }
+        void      MarkAsAppendedInMessage(void) { mLease |= kAppendedInMsgFlag; }
+        void      ClearAppendedInMessageFlag(void) { mLease &= ~kAppendedInMsgFlag; }
         bool      Matches(const Service &aOther) const;
         bool      Matches(ItemState aState) const { return GetState() == aState; }
     };
@@ -343,12 +373,26 @@
      * Config option `OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_DEFAULT_MODE` specifies the default auto-start mode
      * (whether it is enabled or disabled at the start of OT stack).
      *
-     * When auto-start is enabled, the SRP client will monitor the Thread Network Data for SRP Server Service entries
-     * and automatically start and stop the client when an SRP server is detected.
+     * When auto-start is enabled, the SRP client will monitor the Thread Network Data to discover SRP servers and
+     * select the preferred server and automatically start and stop the client when an SRP server is detected.
      *
-     * If multiple SRP servers are found, a random one will be selected. If the selected SRP server is no longer
-     * detected (not longer present in the Thread Network Data), the SRP client will be stopped and then it may switch
-     * to another SRP server (if available).
+     * There are three categories of Network Data entries indicating presence of SRP sever. They are preferred in the
+     * following order:
+     *
+     *   1) Preferred unicast entries where server address is included in the service data. If there are multiple
+     *      options, the one with numerically lowest IPv6 address is preferred.
+     *
+     *   2) Anycast entries each having a seq number. A larger sequence number in the sense specified by Serial Number
+     *      Arithmetic logic in RFC-1982 is considered more recent and therefore preferred. The largest seq number
+     *      using serial number arithmetic is preferred if it is well-defined (i.e., the seq number is larger than all
+     *      other seq numbers). If it is not well-defined, then the numerically largest seq number is preferred.
+     *
+     *   3) Unicast entries where the server address info is included in server data. If there are multiple options,
+     *      the one with numerically lowest IPv6 address is preferred.
+     *
+     * When there is a change in the Network Data entries, client will check that the currently selected server is
+     * still present in the Network Data and is still the preferred one. Otherwise the client will switch to the new
+     * preferred server or stop if there is none.
      *
      * When the SRP client is explicitly started through a successful call to `Start()`, the given SRP server address
      * in `Start()` will continue to be used regardless of the state of auto-start mode and whether the same SRP
@@ -418,7 +462,7 @@
      * @param[in] aContext         An arbitrary context used with @p aCallback.
      *
      */
-    void SetCallback(Callback aCallback, void *aContext);
+    void SetCallback(ClientCallback aCallback, void *aContext) { mCallback.Set(aCallback, aContext); }
 
     /**
      * This method gets the TTL used in SRP update requests.
@@ -431,7 +475,7 @@
      * @returns The TTL (in seconds).
      *
      */
-    uint32_t GetTtl(void) const { return (0 < mTtl && mTtl < mLeaseInterval) ? mTtl : mLeaseInterval; }
+    uint32_t GetTtl(void) const { return mTtl; }
 
     /**
      * This method sets the TTL used in SRP update requests.
@@ -454,7 +498,7 @@
      * @returns The lease interval (in seconds).
      *
      */
-    uint32_t GetLeaseInterval(void) const { return mLeaseInterval; }
+    uint32_t GetLeaseInterval(void) const { return mDefaultLease; }
 
     /**
      * This method sets the lease interval used in SRP update requests.
@@ -465,7 +509,7 @@
      * @param[in] aInterval  The lease interval (in seconds). If zero, the default value `kDefaultLease` would be used.
      *
      */
-    void SetLeaseInterval(uint32_t aInterval) { mLeaseInterval = GetBoundedLeaseInterval(aInterval, kDefaultLease); }
+    void SetLeaseInterval(uint32_t aInterval) { mDefaultLease = DetermineLeaseInterval(aInterval, kDefaultLease); }
 
     /**
      * This method gets the key lease interval used in SRP update requests.
@@ -473,7 +517,7 @@
      * @returns The key lease interval (in seconds).
      *
      */
-    uint32_t GetKeyLeaseInterval(void) const { return mKeyLeaseInterval; }
+    uint32_t GetKeyLeaseInterval(void) const { return mDefaultKeyLease; }
 
     /**
      * This method sets the key lease interval used in SRP update requests.
@@ -487,7 +531,7 @@
      */
     void SetKeyLeaseInterval(uint32_t aInterval)
     {
-        mKeyLeaseInterval = GetBoundedLeaseInterval(aInterval, kDefaultKeyLease);
+        mDefaultKeyLease = DetermineLeaseInterval(aInterval, kDefaultKeyLease);
     }
 
     /**
@@ -722,12 +766,38 @@
      *
      */
     bool IsServiceKeyRecordEnabled(void) const { return mServiceKeyRecordEnabled; }
+
+    /**
+     * This method enables/disables "use short Update Lease Option" behavior.
+     *
+     * When enabled, the SRP client will use the short variant format of Update Lease Option in its message. The short
+     * format only includes the lease interval.
+     *
+     * This method is added under `REFERENCE_DEVICE` config and is intended to override the default behavior for
+     * testing only.
+     *
+     * @param[in] aUseShort    TRUE to enable, FALSE to disable the "use short Update Lease Option" mode.
+     *
+     */
+    void SetUseShortLeaseOption(bool aUseShort) { mUseShortLeaseOption = aUseShort; }
+
+    /**
+     * This method gets the current "use short Update Lease Option" mode.
+     *
+     * @returns TRUE if "use short Update Lease Option" mode is enabled, FALSE otherwise.
+     *
+     */
+    bool GetUseShortLeaseOption(void) const { return mUseShortLeaseOption; }
 #endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
 
 private:
     // 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;
@@ -836,6 +906,8 @@
     // Port number to use when server is discovered using "network data anycast service".
     static constexpr uint16_t kAnycastServerPort = 53;
 
+    static constexpr uint32_t kUnspecifiedInterval = 0; // Used for lease/key-lease intervals.
+
     // This enumeration type is used by the private `Start()` and
     // `Stop()` methods to indicate whether it is being requested by the
     // user or by the auto-start feature.
@@ -855,28 +927,15 @@
         kKeepRetryInterval,
     };
 
-    class SingleServiceMode
+    // Used in `ChangeHostAndServiceStates()`
+    enum ServiceStateChangeMode : uint8_t
     {
-    public:
-        SingleServiceMode(void)
-            : mEnabled(false)
-            , mService(nullptr)
-        {
-        }
-
-        void     Enable(void) { mEnabled = true, mService = nullptr; }
-        void     Disable(void) { mEnabled = false; }
-        bool     IsEnabled(void) const { return mEnabled; }
-        Service *GetService(void) { return mService; }
-        void     SetService(Service &aService) { mService = &aService; }
-
-    private:
-        bool     mEnabled;
-        Service *mService;
+        kForAllServices,
+        kForServicesAppendedInMessage,
     };
 
 #if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
-    class AutoStart : Clearable<AutoStart>
+    class AutoStart : public Clearable<AutoStart>
     {
     public:
         enum State : uint8_t{
@@ -893,17 +952,17 @@
         void    SetState(State aState);
         uint8_t GetAnycastSeqNum(void) const { return mAnycastSeqNum; }
         void    SetAnycastSeqNum(uint8_t aAnycastSeqNum) { mAnycastSeqNum = aAnycastSeqNum; }
-        void    SetCallback(AutoStartCallback aCallback, void *aContext);
+        void    SetCallback(AutoStartCallback aCallback, void *aContext) { mCallback.Set(aCallback, aContext); }
         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
@@ -913,12 +972,11 @@
 
         static const char *StateToString(State aState);
 
-        AutoStartCallback mCallback;
-        void *            mContext;
-        State             mState;
-        uint8_t           mAnycastSeqNum;
+        Callback<AutoStartCallback> mCallback;
+        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
@@ -927,36 +985,46 @@
     {
         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);
-    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        AppendServiceInstructions(Service &aService, Message &aMessage, Info &aInfo);
+    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);
     Error        AppendHostDescriptionInstruction(Message &aMessage, Info &aInfo);
     Error        AppendKeyRecord(Message &aMessage, Info &aInfo) const;
     Error        AppendDeleteAllRrsets(Message &aMessage) const;
     Error        AppendHostName(Message &aMessage, Info &aInfo, bool aDoNotCompress = false) const;
     Error        AppendAaaaRecord(const Ip6::Address &aAddress, Message &aMessage, Info &aInfo) const;
-    Error        AppendUpdateLeaseOptRecord(Message &aMessage) const;
+    Error        AppendUpdateLeaseOptRecord(Message &aMessage);
     Error        AppendSignature(Message &aMessage, Info &aInfo);
     void         UpdateRecordLengthInMessage(Dns::ResourceRecord &aRecord, uint16_t aOffset, Message &aMessage) const;
     static void  HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
@@ -969,9 +1037,9 @@
     uint32_t     GetRetryWaitInterval(void) const { return mRetryWaitInterval; }
     void         ResetRetryWaitInterval(void) { mRetryWaitInterval = kMinRetryWaitInterval; }
     void         GrowRetryWaitInterval(void);
-    uint32_t     GetBoundedLeaseInterval(uint32_t aInterval, uint32_t aDefaultInterval) const;
+    uint32_t     DetermineLeaseInterval(uint32_t aInterval, uint32_t aDefaultInterval) const;
+    uint32_t     DetermineTtl(void) const;
     bool         ShouldRenewEarly(const Service &aService) const;
-    static void  HandleTimer(Timer &aTimer);
     void         HandleTimer(void);
 #if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
     void  ProcessAutoStart(void);
@@ -992,32 +1060,35 @@
 
     static_assert(kMaxTxFailureRetries < 16, "kMaxTxFailureRetries exceed the range of mTxFailureRetryCount (4-bit)");
 
+    using DelayTimer = TimerMilliIn<Client, &Client::HandleTimer>;
+
     State   mState;
     uint8_t mTxFailureRetryCount : 4;
     bool    mShouldRemoveKeyLease : 1;
     bool    mAutoHostAddressAddedMeshLocal : 1;
+    bool    mSingleServiceMode : 1;
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     bool mServiceKeyRecordEnabled : 1;
+    bool mUseShortLeaseOption : 1;
 #endif
 
     uint16_t mUpdateMessageId;
     uint32_t mRetryWaitInterval;
 
     TimeMilli mLeaseRenewTime;
-    uint32_t  mAcceptedLeaseInterval;
     uint32_t  mTtl;
-    uint32_t  mLeaseInterval;
-    uint32_t  mKeyLeaseInterval;
+    uint32_t  mLease;
+    uint32_t  mKeyLease;
+    uint32_t  mDefaultLease;
+    uint32_t  mDefaultKeyLease;
 
     Ip6::Udp::Socket mSocket;
 
-    Callback            mCallback;
-    void *              mCallbackContext;
-    const char *        mDomainName;
-    HostInfo            mHostInfo;
-    LinkedList<Service> mServices;
-    SingleServiceMode   mSingleServiceMode;
-    TimerMilli          mTimer;
+    Callback<ClientCallback> mCallback;
+    const char              *mDomainName;
+    HostInfo                 mHostInfo;
+    LinkedList<Service>      mServices;
+    DelayTimer               mTimer;
 #if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
     AutoStart mAutoStart;
 #endif
diff --git a/src/core/net/srp_server.cpp b/src/core/net/srp_server.cpp
index cdcee31..7125238 100644
--- a/src/core/net/srp_server.cpp
+++ b/src/core/net/srp_server.cpp
@@ -41,6 +41,7 @@
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
 #include "common/new.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 #include "net/dns_types.hpp"
 #include "thread/thread_netif.hpp"
@@ -85,26 +86,21 @@
 Server::Server(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mSocket(aInstance)
-    , mServiceUpdateHandler(nullptr)
-    , mServiceUpdateHandlerContext(nullptr)
-    , mLeaseTimer(aInstance, HandleLeaseTimer)
-    , mOutstandingUpdatesTimer(aInstance, HandleOutstandingUpdatesTimer)
+    , mLeaseTimer(aInstance)
+    , mOutstandingUpdatesTimer(aInstance)
     , mServiceUpdateId(Random::NonCrypto::GetUint32())
     , mPort(kUdpPortMin)
     , mState(kStateDisabled)
     , mAddressMode(kDefaultAddressMode)
     , mAnycastSequenceNumber(0)
     , mHasRegisteredAnyService(false)
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    , mAutoEnable(false)
+#endif
 {
     IgnoreError(SetDomain(kDefaultDomain));
 }
 
-void Server::SetServiceHandler(otSrpServerServiceUpdateHandler aServiceHandler, void *aServiceHandlerContext)
-{
-    mServiceUpdateHandler        = aServiceHandler;
-    mServiceUpdateHandlerContext = aServiceHandlerContext;
-}
-
 Error Server::SetAddressMode(AddressMode aMode)
 {
     Error error = kErrorNone;
@@ -133,41 +129,71 @@
 
 void Server::SetEnabled(bool aEnabled)
 {
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    mAutoEnable = false;
+#endif
+
     if (aEnabled)
     {
-        VerifyOrExit(mState == kStateDisabled);
-        mState = kStateStopped;
-
-        // Request publishing of "DNS/SRP Address Service" entry in the
-        // Thread Network Data based of `mAddressMode`. Then wait for
-        // callback `HandleNetDataPublisherEntryChange()` from the
-        // `Publisher` to start the SRP server.
-
-        switch (mAddressMode)
-        {
-        case kAddressModeUnicast:
-            SelectPort();
-            Get<NetworkData::Publisher>().PublishDnsSrpServiceUnicast(mPort);
-            break;
-
-        case kAddressModeAnycast:
-            mPort = kAnycastAddressModePort;
-            Get<NetworkData::Publisher>().PublishDnsSrpServiceAnycast(mAnycastSequenceNumber);
-            break;
-        }
+        Enable();
     }
     else
     {
-        VerifyOrExit(mState != kStateDisabled);
-        Get<NetworkData::Publisher>().UnpublishDnsSrpService();
-        Stop();
-        mState = kStateDisabled;
+        Disable();
+    }
+}
+
+void Server::Enable(void)
+{
+    VerifyOrExit(mState == kStateDisabled);
+    mState = kStateStopped;
+
+    // Request publishing of "DNS/SRP Address Service" entry in the
+    // Thread Network Data based of `mAddressMode`. Then wait for
+    // callback `HandleNetDataPublisherEntryChange()` from the
+    // `Publisher` to start the SRP server.
+
+    switch (mAddressMode)
+    {
+    case kAddressModeUnicast:
+        SelectPort();
+        Get<NetworkData::Publisher>().PublishDnsSrpServiceUnicast(mPort);
+        break;
+
+    case kAddressModeAnycast:
+        mPort = kAnycastAddressModePort;
+        Get<NetworkData::Publisher>().PublishDnsSrpServiceAnycast(mAnycastSequenceNumber);
+        break;
     }
 
 exit:
     return;
 }
 
+void Server::Disable(void)
+{
+    VerifyOrExit(mState != kStateDisabled);
+    Get<NetworkData::Publisher>().UnpublishDnsSrpService();
+    Stop();
+    mState = kStateDisabled;
+
+exit:
+    return;
+}
+
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+void Server::SetAutoEnableMode(bool aEnabled)
+{
+    VerifyOrExit(mAutoEnable != aEnabled);
+    mAutoEnable = aEnabled;
+
+    Get<BorderRouter::RoutingManager>().HandleSrpServerAutoEnableMode();
+
+exit:
+    return;
+}
+#endif
+
 Server::TtlConfig::TtlConfig(void)
 {
     mMinTtl = kDefaultMinTtl;
@@ -189,7 +215,7 @@
 {
     OT_ASSERT(mMinTtl <= mMaxTtl);
 
-    return OT_MAX(mMinTtl, OT_MIN(OT_MIN(mMaxTtl, aLease), aTtl));
+    return Clamp(Min(aTtl, aLease), mMinTtl, mMaxTtl);
 }
 
 Server::LeaseConfig::LeaseConfig(void)
@@ -222,14 +248,14 @@
 {
     OT_ASSERT(mMinLease <= mMaxLease);
 
-    return (aLease == 0) ? 0 : OT_MAX(mMinLease, OT_MIN(mMaxLease, aLease));
+    return (aLease == 0) ? 0 : Clamp(aLease, mMinLease, mMaxLease);
 }
 
 uint32_t Server::LeaseConfig::GrantKeyLease(uint32_t aKeyLease) const
 {
     OT_ASSERT(mMinKeyLease <= mMaxKeyLease);
 
-    return (aKeyLease == 0) ? 0 : OT_MAX(mMinKeyLease, OT_MIN(mMaxKeyLease, aKeyLease));
+    return (aKeyLease == 0) ? 0 : Clamp(aKeyLease, mMinKeyLease, mMaxKeyLease);
 }
 
 Error Server::SetLeaseConfig(const LeaseConfig &aLeaseConfig)
@@ -308,12 +334,12 @@
         LogInfo("Fully remove host %s", aHost->GetFullName());
     }
 
-    if (aNotifyServiceHandler && mServiceUpdateHandler != nullptr)
+    if (aNotifyServiceHandler && mServiceUpdateHandler.IsSet())
     {
         uint32_t updateId = AllocateId();
 
-        LogInfo("SRP update handler is notified (updatedId = %u)", updateId);
-        mServiceUpdateHandler(updateId, aHost, kDefaultEventsHandlerTimeout, mServiceUpdateHandlerContext);
+        LogInfo("SRP update handler is notified (updatedId = %lu)", ToUlong(updateId));
+        mServiceUpdateHandler.Invoke(updateId, aHost, static_cast<uint32_t>(kDefaultEventsHandlerTimeout));
         // We don't wait for the reply from the service update handler,
         // but always remove the host (and its services) regardless of
         // host/service update result. Because removing a host should fail
@@ -372,13 +398,14 @@
     }
     else
     {
-        LogInfo("Delayed SRP host update result, the SRP update has been committed (updateId = %u)", aId);
+        LogInfo("Delayed SRP host update result, the SRP update has been committed (updateId = %lu)", ToUlong(aId));
     }
 }
 
 void Server::HandleServiceUpdateResult(UpdateMetadata *aUpdate, Error aError)
 {
-    LogInfo("Handler result of SRP update (id = %u) is received: %s", aUpdate->GetId(), ErrorToString(aError));
+    LogInfo("Handler result of SRP update (id = %lu) is received: %s", ToUlong(aUpdate->GetId()),
+            ErrorToString(aError));
 
     IgnoreError(mOutstandingUpdates.Remove(*aUpdate));
     CommitSrpUpdate(aError, *aUpdate);
@@ -408,26 +435,26 @@
 }
 
 void Server::CommitSrpUpdate(Error                    aError,
-                             Host &                   aHost,
+                             Host                    &aHost,
                              const Dns::UpdateHeader &aDnsHeader,
-                             const Ip6::MessageInfo * aMessageInfo,
-                             const TtlConfig &        aTtlConfig,
-                             const LeaseConfig &      aLeaseConfig)
+                             const Ip6::MessageInfo  *aMessageInfo,
+                             const TtlConfig         &aTtlConfig,
+                             const LeaseConfig       &aLeaseConfig)
 {
-    Host *   existingHost;
-    uint32_t hostLease;
-    uint32_t hostKeyLease;
-    uint32_t grantedLease;
-    uint32_t grantedKeyLease;
+    Host    *existingHost;
     uint32_t grantedTtl;
-    bool     shouldFreeHost = true;
+    uint32_t hostLease       = 0;
+    uint32_t hostKeyLease    = 0;
+    uint32_t grantedLease    = 0;
+    uint32_t grantedKeyLease = 0;
+    bool     shouldFreeHost  = true;
 
     SuccessOrExit(aError);
 
     hostLease       = aHost.GetLease();
     hostKeyLease    = aHost.GetKeyLease();
     grantedLease    = aLeaseConfig.GrantLease(hostLease);
-    grantedKeyLease = aLeaseConfig.GrantKeyLease(hostKeyLease);
+    grantedKeyLease = aHost.ShouldUseShortLeaseOption() ? grantedLease : aLeaseConfig.GrantKeyLease(hostKeyLease);
     grantedTtl      = aTtlConfig.GrantTtl(grantedLease, aHost.GetTtl());
 
     aHost.SetLease(grantedLease);
@@ -496,7 +523,7 @@
     {
         if (aError == kErrorNone && !(grantedLease == hostLease && grantedKeyLease == hostKeyLease))
         {
-            SendResponse(aDnsHeader, grantedLease, grantedKeyLease, *aMessageInfo);
+            SendResponse(aDnsHeader, grantedLease, grantedKeyLease, aHost.ShouldUseShortLeaseOption(), *aMessageInfo);
         }
         else
         {
@@ -564,7 +591,7 @@
 
     VerifyOrExit(!mSocket.IsOpen());
     SuccessOrExit(error = mSocket.Open(HandleUdpReceive, this));
-    error = mSocket.Bind(mPort, OT_NETIF_THREAD);
+    error = mSocket.Bind(mPort, Ip6::kNetifThread);
 
 exit:
     if (error != kErrorNone)
@@ -700,7 +727,7 @@
 
     if (FindOutstandingUpdate(aMetadata) != nullptr)
     {
-        LogInfo("Drop duplicated SRP update request: MessageId=%hu", aMetadata.mDnsHeader.GetMessageId());
+        LogInfo("Drop duplicated SRP update request: MessageId=%u", aMetadata.mDnsHeader.GetMessageId());
 
         // Silently drop duplicate requests.
         // This could rarely happen, because the outstanding SRP update timer should
@@ -793,11 +820,11 @@
     return error;
 }
 
-Error Server::ProcessHostDescriptionInstruction(Host &                 aHost,
-                                                const Message &        aMessage,
+Error Server::ProcessHostDescriptionInstruction(Host                  &aHost,
+                                                const Message         &aMessage,
                                                 const MessageMetadata &aMetadata) const
 {
-    Error    error;
+    Error    error  = kErrorNone;
     uint16_t offset = aMetadata.mOffset;
 
     OT_ASSERT(aHost.GetFullName() == nullptr);
@@ -879,8 +906,8 @@
     return error;
 }
 
-Error Server::ProcessServiceDiscoveryInstructions(Host &                 aHost,
-                                                  const Message &        aMessage,
+Error Server::ProcessServiceDiscoveryInstructions(Host                  &aHost,
+                                                  const Message         &aMessage,
                                                   const MessageMetadata &aMetadata) const
 {
     Error    error  = kErrorNone;
@@ -891,8 +918,8 @@
         char           serviceName[Dns::Name::kMaxNameSize];
         char           instanceName[Dns::Name::kMaxNameSize];
         Dns::PtrRecord ptrRecord;
-        const char *   subServiceName;
-        Service *      service;
+        const char    *subServiceName;
+        Service       *service;
         bool           isSubType;
 
         SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, serviceName, sizeof(serviceName)));
@@ -930,10 +957,8 @@
         }
 
         // Verify that instance name and service name are related.
-
-        VerifyOrExit(
-            StringEndsWith(instanceName, isSubType ? subServiceName : serviceName, kStringCaseInsensitiveMatch),
-            error = kErrorFailed);
+        VerifyOrExit(Dns::Name::IsSubDomainOf(instanceName, isSubType ? subServiceName : serviceName),
+                     error = kErrorFailed);
 
         // Ensure the same service does not exist already.
         VerifyOrExit(aHost.FindService(serviceName, instanceName) == nullptr, error = kErrorFailed);
@@ -959,8 +984,8 @@
     return error;
 }
 
-Error Server::ProcessServiceDescriptionInstructions(Host &           aHost,
-                                                    const Message &  aMessage,
+Error Server::ProcessServiceDescriptionInstructions(Host            &aHost,
+                                                    const Message   &aMessage,
                                                     MessageMetadata &aMetadata) const
 {
     Error    error  = kErrorNone;
@@ -1091,15 +1116,18 @@
 
     SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, name, sizeof(name)));
     SuccessOrExit(error = aMessage.Read(offset, optRecord));
-    SuccessOrExit(error = aMessage.Read(offset + sizeof(optRecord), leaseOption));
-    VerifyOrExit(leaseOption.IsValid(), error = kErrorFailed);
-    VerifyOrExit(optRecord.GetSize() == sizeof(optRecord) + sizeof(leaseOption), error = kErrorParse);
+
+    SuccessOrExit(error = leaseOption.ReadFrom(aMessage, offset + sizeof(optRecord), optRecord.GetLength()));
 
     offset += optRecord.GetSize();
 
     aHost->SetLease(leaseOption.GetLeaseInterval());
     aHost->SetKeyLease(leaseOption.GetKeyLeaseInterval());
 
+    // If the client included the short variant of Lease Option,
+    // server must also use the short variant in its response.
+    aHost->SetUseShortLeaseOption(leaseOption.IsShortVariant());
+
     if (aHost->GetLease() > 0)
     {
         uint8_t hostAddressesNum;
@@ -1150,12 +1178,12 @@
 }
 
 Error Server::VerifySignature(const Dns::Ecdsa256KeyRecord &aKeyRecord,
-                              const Message &               aMessage,
+                              const Message                &aMessage,
                               Dns::UpdateHeader             aDnsHeader,
                               uint16_t                      aSigOffset,
                               uint16_t                      aSigRdataOffset,
                               uint16_t                      aSigRdataLength,
-                              const char *                  aSignerName) const
+                              const char                   *aSignerName) const
 {
     Error                          error;
     uint16_t                       offset = aMessage.GetOffset();
@@ -1163,7 +1191,7 @@
     Crypto::Sha256                 sha256;
     Crypto::Sha256::Hash           hash;
     Crypto::Ecdsa::P256::Signature signature;
-    Message *                      signerNameMessage = nullptr;
+    Message                       *signerNameMessage = nullptr;
 
     VerifyOrExit(aSigRdataLength >= Crypto::Ecdsa::P256::Signature::kSize, error = kErrorInvalidArgs);
 
@@ -1174,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());
@@ -1297,7 +1325,50 @@
 
 void Server::InformUpdateHandlerOrCommit(Error aError, Host &aHost, const MessageMetadata &aMetadata)
 {
-    if ((aError == kErrorNone) && (mServiceUpdateHandler != nullptr))
+#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
+    if (aError == kErrorNone)
+    {
+        uint8_t             numAddrs;
+        const Ip6::Address *addrs;
+
+        LogInfo("Processed DNS update info");
+        LogInfo("    Host:%s", aHost.GetFullName());
+        LogInfo("    Lease:%lu, key-lease:%lu, ttl:%lu", ToUlong(aHost.GetLease()), ToUlong(aHost.GetKeyLease()),
+                ToUlong(aHost.GetTtl()));
+
+        addrs = aHost.GetAddresses(numAddrs);
+
+        if (numAddrs == 0)
+        {
+            LogInfo("    No host address");
+        }
+        else
+        {
+            LogInfo("    %d host address(es):", numAddrs);
+
+            for (; numAddrs > 0; addrs++, numAddrs--)
+            {
+                LogInfo("      %s", addrs->ToString().AsCString());
+            }
+        }
+
+        for (const Service &service : aHost.GetServices())
+        {
+            char subLabel[Dns::Name::kMaxLabelSize];
+
+            IgnoreError(service.GetServiceSubTypeLabel(subLabel, sizeof(subLabel)));
+
+            LogInfo("    %s service '%s'%s%s", service.IsDeleted() ? "Deleting" : "Adding", service.GetInstanceName(),
+                    service.IsSubType() ? " subtype:" : "", subLabel);
+        }
+    }
+    else
+    {
+        LogInfo("Error %s processing received DNS update", ErrorToString(aError));
+    }
+#endif // OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
+
+    if ((aError == kErrorNone) && mServiceUpdateHandler.IsSet())
     {
         UpdateMetadata *update = UpdateMetadata::Allocate(GetInstance(), aHost, aMetadata);
 
@@ -1306,8 +1377,8 @@
             mOutstandingUpdates.Push(*update);
             mOutstandingUpdatesTimer.FireAtIfEarlier(update->GetExpireTime());
 
-            LogInfo("SRP update handler is notified (updatedId = %u)", update->GetId());
-            mServiceUpdateHandler(update->GetId(), &aHost, kDefaultEventsHandlerTimeout, mServiceUpdateHandlerContext);
+            LogInfo("SRP update handler is notified (updatedId = %lu)", ToUlong(update->GetId()));
+            mServiceUpdateHandler.Invoke(update->GetId(), &aHost, static_cast<uint32_t>(kDefaultEventsHandlerTimeout));
             ExitNow();
         }
 
@@ -1320,15 +1391,15 @@
     return;
 }
 
-void Server::SendResponse(const Dns::UpdateHeader &   aHeader,
+void Server::SendResponse(const Dns::UpdateHeader    &aHeader,
                           Dns::UpdateHeader::Response aResponseCode,
-                          const Ip6::MessageInfo &    aMessageInfo)
+                          const Ip6::MessageInfo     &aMessageInfo)
 {
     Error             error;
-    Message *         response = nullptr;
+    Message          *response = nullptr;
     Dns::UpdateHeader header;
 
-    response = GetSocket().NewMessage(0);
+    response = GetSocket().NewMessage();
     VerifyOrExit(response != nullptr, error = kErrorNoBufs);
 
     header.SetMessageId(aHeader.GetMessageId());
@@ -1361,15 +1432,17 @@
 void Server::SendResponse(const Dns::UpdateHeader &aHeader,
                           uint32_t                 aLease,
                           uint32_t                 aKeyLease,
-                          const Ip6::MessageInfo & aMessageInfo)
+                          bool                     mUseShortLeaseOption,
+                          const Ip6::MessageInfo  &aMessageInfo)
 {
     Error             error;
-    Message *         response = nullptr;
+    Message          *response = nullptr;
     Dns::UpdateHeader header;
     Dns::OptRecord    optRecord;
     Dns::LeaseOption  leaseOption;
+    uint16_t          optionSize;
 
-    response = GetSocket().NewMessage(0);
+    response = GetSocket().NewMessage();
     VerifyOrExit(response != nullptr, error = kErrorNoBufs);
 
     header.SetMessageId(aHeader.GetMessageId());
@@ -1385,17 +1458,25 @@
     optRecord.Init();
     optRecord.SetUdpPayloadSize(kUdpPayloadSize);
     optRecord.SetDnsSecurityFlag();
-    optRecord.SetLength(sizeof(Dns::LeaseOption));
-    SuccessOrExit(error = response->Append(optRecord));
 
-    leaseOption.Init();
-    leaseOption.SetLeaseInterval(aLease);
-    leaseOption.SetKeyLeaseInterval(aKeyLease);
-    SuccessOrExit(error = response->Append(leaseOption));
+    if (mUseShortLeaseOption)
+    {
+        leaseOption.InitAsShortVariant(aLease);
+    }
+    else
+    {
+        leaseOption.InitAsLongVariant(aLease, aKeyLease);
+    }
+
+    optionSize = static_cast<uint16_t>(leaseOption.GetSize());
+    optRecord.SetLength(optionSize);
+
+    SuccessOrExit(error = response->Append(optRecord));
+    SuccessOrExit(error = response->AppendBytes(&leaseOption, optionSize));
 
     SuccessOrExit(error = GetSocket().SendTo(*response, aMessageInfo));
 
-    LogInfo("Send success response with granted lease: %u and key lease: %u", aLease, aKeyLease);
+    LogInfo("Send success response with granted lease: %lu and key lease: %lu", ToUlong(aLease), ToUlong(aKeyLease));
 
     UpdateResponseCounters(Dns::UpdateHeader::kResponseSuccess);
 
@@ -1427,10 +1508,10 @@
     return ProcessMessage(aMessage, TimerMilli::GetNow(), mTtlConfig, mLeaseConfig, &aMessageInfo);
 }
 
-Error Server::ProcessMessage(Message &               aMessage,
+Error Server::ProcessMessage(Message                &aMessage,
                              TimeMilli               aRxTime,
-                             const TtlConfig &       aTtlConfig,
-                             const LeaseConfig &     aLeaseConfig,
+                             const TtlConfig        &aTtlConfig,
+                             const LeaseConfig      &aLeaseConfig,
                              const Ip6::MessageInfo *aMessageInfo)
 {
     Error           error;
@@ -1454,16 +1535,11 @@
     return error;
 }
 
-void Server::HandleLeaseTimer(Timer &aTimer)
-{
-    aTimer.Get<Server>().HandleLeaseTimer();
-}
-
 void Server::HandleLeaseTimer(void)
 {
     TimeMilli now                = TimerMilli::GetNow();
     TimeMilli earliestExpireTime = now.GetDistantFuture();
-    Host *    nextHost;
+    Host     *nextHost;
 
     for (Host *host = mHosts.GetHead(); host != nullptr; host = nextHost)
     {
@@ -1482,7 +1558,7 @@
 
             Service *next;
 
-            earliestExpireTime = OT_MIN(earliestExpireTime, host->GetKeyExpireTime());
+            earliestExpireTime = Min(earliestExpireTime, host->GetKeyExpireTime());
 
             // Check if any service instance name expired.
             for (Service *service = host->mServices.GetHead(); service != nullptr; service = next)
@@ -1498,7 +1574,7 @@
                 }
                 else
                 {
-                    earliestExpireTime = OT_MIN(earliestExpireTime, service->GetKeyExpireTime());
+                    earliestExpireTime = Min(earliestExpireTime, service->GetKeyExpireTime());
                 }
             }
         }
@@ -1515,7 +1591,7 @@
 
             RemoveHost(host, kRetainName, kNotifyServiceHandler);
 
-            earliestExpireTime = OT_MIN(earliestExpireTime, host->GetKeyExpireTime());
+            earliestExpireTime = Min(earliestExpireTime, host->GetKeyExpireTime());
         }
         else
         {
@@ -1525,7 +1601,7 @@
 
             OT_ASSERT(!host->IsDeleted());
 
-            earliestExpireTime = OT_MIN(earliestExpireTime, host->GetExpireTime());
+            earliestExpireTime = Min(earliestExpireTime, host->GetExpireTime());
 
             for (Service *service = host->mServices.GetHead(); service != nullptr; service = next)
             {
@@ -1539,7 +1615,7 @@
                 else if (service->mIsDeleted)
                 {
                     // The service has been deleted but the name retains.
-                    earliestExpireTime = OT_MIN(earliestExpireTime, service->GetKeyExpireTime());
+                    earliestExpireTime = Min(earliestExpireTime, service->GetKeyExpireTime());
                 }
                 else if (service->GetExpireTime() <= now)
                 {
@@ -1547,11 +1623,11 @@
 
                     // The service is expired, delete it.
                     host->RemoveService(service, kRetainName, kNotifyServiceHandler);
-                    earliestExpireTime = OT_MIN(earliestExpireTime, service->GetKeyExpireTime());
+                    earliestExpireTime = Min(earliestExpireTime, service->GetKeyExpireTime());
                 }
                 else
                 {
-                    earliestExpireTime = OT_MIN(earliestExpireTime, service->GetExpireTime());
+                    earliestExpireTime = Min(earliestExpireTime, service->GetExpireTime());
                 }
             }
         }
@@ -1562,7 +1638,7 @@
         OT_ASSERT(earliestExpireTime >= now);
         if (!mLeaseTimer.IsRunning() || earliestExpireTime <= mLeaseTimer.GetFireTime())
         {
-            LogInfo("Lease timer is scheduled for %u seconds", Time::MsecToSec(earliestExpireTime - now));
+            LogInfo("Lease timer is scheduled for %lu seconds", ToUlong(Time::MsecToSec(earliestExpireTime - now)));
             mLeaseTimer.StartAt(earliestExpireTime, 0);
         }
     }
@@ -1573,16 +1649,11 @@
     }
 }
 
-void Server::HandleOutstandingUpdatesTimer(Timer &aTimer)
-{
-    aTimer.Get<Server>().HandleOutstandingUpdatesTimer();
-}
-
 void Server::HandleOutstandingUpdatesTimer(void)
 {
     while (!mOutstandingUpdates.IsEmpty() && mOutstandingUpdates.GetTail()->GetExpireTime() <= TimerMilli::GetNow())
     {
-        LogInfo("Outstanding service update timeout (updateId = %u)", mOutstandingUpdates.GetTail()->GetId());
+        LogInfo("Outstanding service update timeout (updateId = %lu)", ToUlong(mOutstandingUpdates.GetTail()->GetId()));
         HandleServiceUpdateResult(mOutstandingUpdates.GetTail(), kErrorResponseTimeout);
     }
 }
@@ -1686,13 +1757,22 @@
 void Server::Service::GetLeaseInfo(LeaseInfo &aLeaseInfo) const
 {
     TimeMilli now           = TimerMilli::GetNow();
-    TimeMilli expireTime    = GetExpireTime();
     TimeMilli keyExpireTime = GetKeyExpireTime();
 
     aLeaseInfo.mLease             = Time::SecToMsec(GetLease());
     aLeaseInfo.mKeyLease          = Time::SecToMsec(GetKeyLease());
-    aLeaseInfo.mRemainingLease    = (now <= expireTime) ? (expireTime - now) : 0;
     aLeaseInfo.mRemainingKeyLease = (now <= keyExpireTime) ? (keyExpireTime - now) : 0;
+
+    if (!mIsDeleted)
+    {
+        TimeMilli expireTime = GetExpireTime();
+
+        aLeaseInfo.mRemainingLease = (now <= expireTime) ? (expireTime - now) : 0;
+    }
+    else
+    {
+        aLeaseInfo.mRemainingLease = 0;
+    }
 }
 
 bool Server::Service::MatchesInstanceName(const char *aInstanceName) const
@@ -1741,7 +1821,7 @@
         "Update existing",           // (1) kUpdateExisting
         "Remove but retain name of", // (2) kRemoveButRetainName
         "Fully remove",              // (3) kFullyRemove
-        "LEASE expired for ",        // (4) kLeaseExpired
+        "LEASE expired for",         // (4) kLeaseExpired
         "KEY LEASE expired for",     // (5) kKeyLeaseExpired
     };
 
@@ -1768,9 +1848,7 @@
     }
 }
 #else
-void Server::Service::Log(Action) const
-{
-}
+void Server::Service::Log(Action) const {}
 #endif // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
 //---------------------------------------------------------------------------------------------------------------------
@@ -1848,10 +1926,7 @@
     mKeyRecord.Clear();
 }
 
-Server::Host::~Host(void)
-{
-    FreeAllServices();
-}
+Server::Host::~Host(void) { FreeAllServices(); }
 
 Error Server::Host::SetFullName(const char *aFullName)
 {
@@ -1892,21 +1967,27 @@
     return mUpdateTime + Time::SecToMsec(mLease);
 }
 
-TimeMilli Server::Host::GetKeyExpireTime(void) const
-{
-    return mUpdateTime + Time::SecToMsec(mKeyLease);
-}
+TimeMilli Server::Host::GetKeyExpireTime(void) const { return mUpdateTime + Time::SecToMsec(mKeyLease); }
 
 void Server::Host::GetLeaseInfo(LeaseInfo &aLeaseInfo) const
 {
     TimeMilli now           = TimerMilli::GetNow();
-    TimeMilli expireTime    = GetExpireTime();
     TimeMilli keyExpireTime = GetKeyExpireTime();
 
     aLeaseInfo.mLease             = Time::SecToMsec(GetLease());
     aLeaseInfo.mKeyLease          = Time::SecToMsec(GetKeyLease());
-    aLeaseInfo.mRemainingLease    = (now <= expireTime) ? (expireTime - now) : 0;
     aLeaseInfo.mRemainingKeyLease = (now <= keyExpireTime) ? (keyExpireTime - now) : 0;
+
+    if (!IsDeleted())
+    {
+        TimeMilli expireTime = GetExpireTime();
+
+        aLeaseInfo.mRemainingLease = (now <= expireTime) ? (expireTime - now) : 0;
+    }
+    else
+    {
+        aLeaseInfo.mRemainingLease = 0;
+    }
 }
 
 Error Server::Host::ProcessTtl(uint32_t aTtl)
@@ -1931,8 +2012,8 @@
 
 const Server::Service *Server::Host::FindNextService(const Service *aPrevService,
                                                      Service::Flags aFlags,
-                                                     const char *   aServiceName,
-                                                     const char *   aInstanceName) const
+                                                     const char    *aServiceName,
+                                                     const char    *aInstanceName) const
 {
     const Service *service = (aPrevService == nullptr) ? GetServices().GetHead() : aPrevService->GetNext();
 
@@ -1964,7 +2045,7 @@
                                              bool        aIsSubType,
                                              TimeMilli   aUpdateTime)
 {
-    Service *                       service = nullptr;
+    Service                        *service = nullptr;
     RetainPtr<Service::Description> desc(FindServiceDescription(aInstanceName));
 
     if (desc == nullptr)
@@ -1992,12 +2073,12 @@
 
     aService->Log(aRetainName ? Service::kRemoveButRetainName : Service::kFullyRemove);
 
-    if (aNotifyServiceHandler && server.mServiceUpdateHandler != nullptr)
+    if (aNotifyServiceHandler && server.mServiceUpdateHandler.IsSet())
     {
         uint32_t updateId = server.AllocateId();
 
-        LogInfo("SRP update handler is notified (updatedId = %u)", updateId);
-        server.mServiceUpdateHandler(updateId, this, kDefaultEventsHandlerTimeout, server.mServiceUpdateHandlerContext);
+        LogInfo("SRP update handler is notified (updatedId = %lu)", ToUlong(updateId));
+        server.mServiceUpdateHandler.Invoke(updateId, this, static_cast<uint32_t>(kDefaultEventsHandlerTimeout));
         // We don't wait for the reply from the service update handler,
         // but always remove the service regardless of service update result.
         // Because removing a service should fail only when there is system
@@ -2042,10 +2123,7 @@
     }
 }
 
-void Server::Host::ClearResources(void)
-{
-    mAddresses.Free();
-}
+void Server::Host::ClearResources(void) { mAddresses.Free(); }
 
 Error Server::Host::MergeServicesAndResourcesFrom(Host &aHost)
 {
@@ -2071,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 b2c3716..b88b136 100644
--- a/src/core/net/srp_server.hpp
+++ b/src/core/net/srp_server.hpp
@@ -55,6 +55,7 @@
 
 #include "common/array.hpp"
 #include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/clearable.hpp"
 #include "common/heap.hpp"
 #include "common/heap_allocatable.hpp"
@@ -65,6 +66,7 @@
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "common/notifier.hpp"
+#include "common/num_utils.hpp"
 #include "common/numeric_limits.hpp"
 #include "common/retain_ptr.hpp"
 #include "common/timer.hpp"
@@ -91,6 +93,12 @@
 }
 } // namespace Dns
 
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+namespace BorderRouter {
+class RoutingManager;
+}
+#endif
+
 namespace Srp {
 
 /**
@@ -104,6 +112,9 @@
     friend class Service;
     friend class Host;
     friend class Dns::ServiceDiscovery::Server;
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    friend class BorderRouter::RoutingManager;
+#endif
 
     enum RetainName : bool
     {
@@ -150,11 +161,15 @@
 
     class Host;
 
+    /**
+     * This enumeration represents the state of SRP server.
+     *
+     */
     enum State : uint8_t
     {
-        kStateDisabled = OT_SRP_SERVER_STATE_DISABLED,
-        kStateRunning  = OT_SRP_SERVER_STATE_RUNNING,
-        kStateStopped  = OT_SRP_SERVER_STATE_STOPPED,
+        kStateDisabled = OT_SRP_SERVER_STATE_DISABLED, ///< Server is disabled.
+        kStateRunning  = OT_SRP_SERVER_STATE_RUNNING,  ///< Server is enabled and running.
+        kStateStopped  = OT_SRP_SERVER_STATE_STOPPED,  ///< Server is enabled but stopped.
     };
 
     /**
@@ -393,7 +408,7 @@
 
             Description *mNext;
             Heap::String mInstanceName;
-            Host *       mHost;
+            Host        *mHost;
             Heap::Data   mTxtData;
             uint16_t     mPriority;
             uint16_t     mWeight;
@@ -421,7 +436,7 @@
 
         Heap::String           mServiceName;
         RetainPtr<Description> mDescription;
-        Service *              mNext;
+        Service               *mNext;
         TimeMilli              mUpdateTime;
         bool                   mIsDeleted : 1;
         bool                   mIsSubType : 1;
@@ -472,7 +487,8 @@
          */
         const Ip6::Address *GetAddresses(uint8_t &aAddressesNum) const
         {
-            aAddressesNum = static_cast<uint8_t>(OT_MIN(mAddresses.GetLength(), NumericLimits<uint8_t>::kMax));
+            aAddressesNum = ClampToUint8(mAddresses.GetLength());
+
             return mAddresses.AsCArray();
         }
 
@@ -555,8 +571,8 @@
          */
         const Service *FindNextService(const Service *aPrevService,
                                        Service::Flags aFlags        = kFlagsAnyService,
-                                       const char *   aServiceName  = nullptr,
-                                       const char *   aInstanceName = nullptr) const;
+                                       const char    *aServiceName  = nullptr,
+                                       const char    *aInstanceName = nullptr) const;
 
         /**
          * This method tells whether the host matches a given full name.
@@ -577,10 +593,12 @@
         void  SetTtl(uint32_t aTtl) { mTtl = aTtl; }
         void  SetLease(uint32_t aLease) { mLease = aLease; }
         void  SetKeyLease(uint32_t aKeyLease) { mKeyLease = aKeyLease; }
+        void  SetUseShortLeaseOption(bool aUse) { mUseShortLeaseOption = aUse; }
+        bool  ShouldUseShortLeaseOption(void) const { return mUseShortLeaseOption; }
         Error ProcessTtl(uint32_t aTtl);
 
         LinkedList<Service> &GetServices(void) { return mServices; }
-        Service *            AddNewService(const char *aServiceName,
+        Service             *AddNewService(const char *aServiceName,
                                            const char *aInstanceName,
                                            bool        aIsSubType,
                                            TimeMilli   aUpdateTime);
@@ -593,12 +611,12 @@
         bool                 HasServiceInstance(const char *aInstanceName) const;
         RetainPtr<Service::Description>       FindServiceDescription(const char *aInstanceName);
         const RetainPtr<Service::Description> FindServiceDescription(const char *aInstanceName) const;
-        Service *                             FindService(const char *aServiceName, const char *aInstanceName);
-        const Service *                       FindService(const char *aServiceName, const char *aInstanceName) const;
-        Service *                             FindBaseService(const char *aInstanceName);
-        const Service *                       FindBaseService(const char *aInstanceName) const;
+        Service                              *FindService(const char *aServiceName, const char *aInstanceName);
+        const Service                        *FindService(const char *aServiceName, const char *aInstanceName) const;
+        Service                              *FindBaseService(const char *aInstanceName);
+        const Service                        *FindBaseService(const char *aInstanceName) const;
 
-        Host *                    mNext;
+        Host                     *mNext;
         Heap::String              mFullName;
         Heap::Array<Ip6::Address> mAddresses;
 
@@ -610,6 +628,7 @@
         uint32_t               mKeyLease; // The KEY-LEASE time in seconds.
         TimeMilli              mUpdateTime;
         LinkedList<Service>    mServices;
+        bool                   mUseShortLeaseOption; // Use short lease option (lease only - 4 byte) when responding.
     };
 
     /**
@@ -703,7 +722,10 @@
      * @sa  HandleServiceUpdateResult
      *
      */
-    void SetServiceHandler(otSrpServerServiceUpdateHandler aServiceHandler, void *aServiceHandlerContext);
+    void SetServiceHandler(otSrpServerServiceUpdateHandler aServiceHandler, void *aServiceHandlerContext)
+    {
+        mServiceUpdateHandler.Set(aServiceHandler, aServiceHandlerContext);
+    }
 
     /**
      * This method returns the domain authorized to the SRP server.
@@ -773,17 +795,9 @@
     Error SetAnycastModeSequenceNumber(uint8_t aSequenceNumber);
 
     /**
-     * This method tells whether the SRP server is currently running.
+     * This method returns the state of the SRP server.
      *
-     * @returns  A boolean that indicates whether the server is running.
-     *
-     */
-    bool IsRunning(void) const { return (mState == kStateRunning); }
-
-    /**
-     * This method tells the state of the SRP server.
-     *
-     * @returns  An enum that represents the state of the server.
+     * @returns  The state of the server.
      *
      */
     State GetState(void) const { return mState; }
@@ -791,10 +805,10 @@
     /**
      * This method tells the port the SRP server is listening to.
      *
-     * @returns  An integer that represents the port of the server. It returns 0 if the SRP server is not running.
+     * @returns  The port of the server or 0 if the SRP server is not running.
      *
      */
-    uint16_t GetPort(void) const { return IsRunning() ? mPort : 0; }
+    uint16_t GetPort(void) const { return (mState == kStateRunning) ? mPort : 0; }
 
     /**
      * This method enables/disables the SRP server.
@@ -804,6 +818,35 @@
      */
     void SetEnabled(bool aEnabled);
 
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    /**
+     * This method enables/disables the auto-enable mode on SRP server.
+     *
+     * When this mode is enabled, the Border Routing Manager controls if/when to enable or disable the SRP server.
+     * SRP sever is auto-enabled if/when Border Routing is started it is done with the initial prefix and route
+     * configurations (when the OMR and on-link prefixes are determined, advertised in emitted Router Advert message on
+     * infrastructure side and published in the Thread Network Data). The SRP server is auto-disabled when BR is
+     * stopped (e.g., if the infrastructure network interface is brought down or if BR gets detached).
+     *
+     * This mode can be disabled by a `SetAutoEnableMode(false)` call or if the SRP server is explicitly enabled or
+     * 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] aEnabled    A boolean to enable/disable the auto-enable mode.
+     *
+     */
+    void SetAutoEnableMode(bool aEnabled);
+
+    /**
+     * This method indicates whether the auto-enable mode is enabled or disabled.
+     *
+     * @retval TRUE   The auto-enable mode is enabled.
+     * @retval FALSE  The auto-enable mode is disabled.
+     *
+     */
+    bool IsAutoEnableMode(void) const { return mAutoEnable; }
+#endif
+
     /**
      * This method returns the TTL configuration.
      *
@@ -877,20 +920,20 @@
 private:
     static constexpr uint16_t kUdpPayloadSize = Ip6::kMaxDatagramLength - sizeof(Ip6::Udp::Header);
 
-    static constexpr uint32_t kDefaultMinTtl               = 60u;             // 1 min (in seconds).
-    static constexpr uint32_t kDefaultMaxTtl               = 3600u * 2;       // 2 hours (in seconds).
-    static constexpr uint32_t kDefaultMinLease             = 60u * 30;        // 30 min (in seconds).
-    static constexpr uint32_t kDefaultMaxLease             = 3600u * 2;       // 2 hours (in seconds).
-    static constexpr uint32_t kDefaultMinKeyLease          = 3600u * 24;      // 1 day (in seconds).
-    static constexpr uint32_t kDefaultMaxKeyLease          = 3600u * 24 * 14; // 14 days (in seconds).
+    static constexpr uint32_t kDefaultMinLease             = 30;          // 30 seconds.
+    static constexpr uint32_t kDefaultMaxLease             = 27u * 3600;  // 27 hours (in seconds).
+    static constexpr uint32_t kDefaultMinKeyLease          = 30;          // 30 seconds.
+    static constexpr uint32_t kDefaultMaxKeyLease          = 189u * 3600; // 189 hours (in seconds).
+    static constexpr uint32_t kDefaultMinTtl               = kDefaultMinLease;
+    static constexpr uint32_t kDefaultMaxTtl               = kDefaultMaxLease;
     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
@@ -919,27 +962,29 @@
         TimeMilli                GetExpireTime(void) const { return mExpireTime; }
         const Dns::UpdateHeader &GetDnsHeader(void) const { return mDnsHeader; }
         ServiceUpdateId          GetId(void) const { return mId; }
-        const TtlConfig &        GetTtlConfig(void) const { return mTtlConfig; }
-        const LeaseConfig &      GetLeaseConfig(void) const { return mLeaseConfig; }
-        Host &                   GetHost(void) { return mHost; }
-        const Ip6::MessageInfo & GetMessageInfo(void) const { return mMessageInfo; }
+        const TtlConfig         &GetTtlConfig(void) const { return mTtlConfig; }
+        const LeaseConfig       &GetLeaseConfig(void) const { return mLeaseConfig; }
+        Host                    &GetHost(void) { return mHost; }
+        const Ip6::MessageInfo  &GetMessageInfo(void) const { return mMessageInfo; }
         bool                     IsDirectRxFromClient(void) const { return mIsDirectRxFromClient; }
         bool                     Matches(ServiceUpdateId aId) const { return mId == aId; }
 
     private:
         UpdateMetadata(Instance &aInstance, Host &aHost, const MessageMetadata &aMessageMetadata);
 
-        UpdateMetadata *  mNext;
+        UpdateMetadata   *mNext;
         TimeMilli         mExpireTime;
         Dns::UpdateHeader mDnsHeader;
         ServiceUpdateId   mId;          // The ID of this service update transaction.
         TtlConfig         mTtlConfig;   // TTL config to use when processing the message.
         LeaseConfig       mLeaseConfig; // Lease config to use when processing the message.
-        Host &            mHost;        // The `UpdateMetadata` has no ownership of this host.
+        Host             &mHost;        // The `UpdateMetadata` has no ownership of this host.
         Ip6::MessageInfo  mMessageInfo; // Valid when `mIsDirectRxFromClient` is true.
         bool              mIsDirectRxFromClient;
     };
 
+    void              Enable(void);
+    void              Disable(void);
     void              Start(void);
     void              Stop(void);
     void              SelectPort(void);
@@ -959,34 +1004,34 @@
     void  CommitSrpUpdate(Error aError, Host &aHost, const MessageMetadata &aMessageMetadata);
     void  CommitSrpUpdate(Error aError, UpdateMetadata &aUpdateMetadata);
     void  CommitSrpUpdate(Error                    aError,
-                          Host &                   aHost,
+                          Host                    &aHost,
                           const Dns::UpdateHeader &aDnsHeader,
-                          const Ip6::MessageInfo * aMessageInfo,
-                          const TtlConfig &        aTtlConfig,
-                          const LeaseConfig &      aLeaseConfig);
+                          const Ip6::MessageInfo  *aMessageInfo,
+                          const TtlConfig         &aTtlConfig,
+                          const LeaseConfig       &aLeaseConfig);
     Error ProcessMessage(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    Error ProcessMessage(Message &               aMessage,
+    Error ProcessMessage(Message                &aMessage,
                          TimeMilli               aRxTime,
-                         const TtlConfig &       aTtlConfig,
-                         const LeaseConfig &     aLeaseConfig,
+                         const TtlConfig        &aTtlConfig,
+                         const LeaseConfig      &aLeaseConfig,
                          const Ip6::MessageInfo *aMessageInfo);
     void  ProcessDnsUpdate(Message &aMessage, MessageMetadata &aMetadata);
     Error ProcessUpdateSection(Host &aHost, const Message &aMessage, MessageMetadata &aMetadata) const;
     Error ProcessAdditionalSection(Host *aHost, const Message &aMessage, MessageMetadata &aMetadata) const;
     Error VerifySignature(const Dns::Ecdsa256KeyRecord &aKeyRecord,
-                          const Message &               aMessage,
+                          const Message                &aMessage,
                           Dns::UpdateHeader             aDnsHeader,
                           uint16_t                      aSigOffset,
                           uint16_t                      aSigRdataOffset,
                           uint16_t                      aSigRdataLength,
-                          const char *                  aSignerName) const;
+                          const char                   *aSignerName) const;
     Error ValidateServiceSubTypes(Host &aHost, const MessageMetadata &aMetadata);
     Error ProcessZoneSection(const Message &aMessage, MessageMetadata &aMetadata) const;
-    Error ProcessHostDescriptionInstruction(Host &                 aHost,
-                                            const Message &        aMessage,
+    Error ProcessHostDescriptionInstruction(Host                  &aHost,
+                                            const Message         &aMessage,
                                             const MessageMetadata &aMetadata) const;
-    Error ProcessServiceDiscoveryInstructions(Host &                 aHost,
-                                              const Message &        aMessage,
+    Error ProcessServiceDiscoveryInstructions(Host                  &aHost,
+                                              const Message         &aMessage,
                                               const MessageMetadata &aMetadata) const;
     Error ProcessServiceDescriptionInstructions(Host &aHost, const Message &aMessage, MessageMetadata &aMetadata) const;
 
@@ -996,29 +1041,32 @@
     void        AddHost(Host &aHost);
     void        RemoveHost(Host *aHost, RetainName aRetainName, NotifyMode aNotifyServiceHandler);
     bool        HasNameConflictsWith(Host &aHost) const;
-    void        SendResponse(const Dns::UpdateHeader &   aHeader,
+    void        SendResponse(const Dns::UpdateHeader    &aHeader,
                              Dns::UpdateHeader::Response aResponseCode,
-                             const Ip6::MessageInfo &    aMessageInfo);
+                             const Ip6::MessageInfo     &aMessageInfo);
     void        SendResponse(const Dns::UpdateHeader &aHeader,
                              uint32_t                 aLease,
                              uint32_t                 aKeyLease,
-                             const Ip6::MessageInfo & aMessageInfo);
+                             bool                     mUseShortLeaseOption,
+                             const Ip6::MessageInfo  &aMessageInfo);
     static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
     void        HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    static void HandleLeaseTimer(Timer &aTimer);
     void        HandleLeaseTimer(void);
     static void HandleOutstandingUpdatesTimer(Timer &aTimer);
     void        HandleOutstandingUpdatesTimer(void);
 
     void                  HandleServiceUpdateResult(UpdateMetadata *aUpdate, Error aError);
     const UpdateMetadata *FindOutstandingUpdate(const MessageMetadata &aMessageMetadata) const;
-    static const char *   AddressModeToString(AddressMode aMode);
+    static const char    *AddressModeToString(AddressMode aMode);
 
     void UpdateResponseCounters(Dns::Header::Response aResponseCode);
 
-    Ip6::Udp::Socket                mSocket;
-    otSrpServerServiceUpdateHandler mServiceUpdateHandler;
-    void *                          mServiceUpdateHandlerContext;
+    using LeaseTimer  = TimerMilliIn<Server, &Server::HandleLeaseTimer>;
+    using UpdateTimer = TimerMilliIn<Server, &Server::HandleOutstandingUpdatesTimer>;
+
+    Ip6::Udp::Socket mSocket;
+
+    Callback<otSrpServerServiceUpdateHandler> mServiceUpdateHandler;
 
     Heap::String mDomain;
 
@@ -1026,9 +1074,9 @@
     LeaseConfig mLeaseConfig;
 
     LinkedList<Host> mHosts;
-    TimerMilli       mLeaseTimer;
+    LeaseTimer       mLeaseTimer;
 
-    TimerMilli                 mOutstandingUpdatesTimer;
+    UpdateTimer                mOutstandingUpdatesTimer;
     LinkedList<UpdateMetadata> mOutstandingUpdates;
 
     ServiceUpdateId mServiceUpdateId;
@@ -1037,6 +1085,9 @@
     AddressMode     mAddressMode;
     uint8_t         mAnycastSequenceNumber;
     bool            mHasRegisteredAnyService : 1;
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    bool mAutoEnable : 1;
+#endif
 
     otSrpServerResponseCounters mResponseCounters;
 };
diff --git a/src/core/net/tcp6.cpp b/src/core/net/tcp6.cpp
index 9b18a06..47c5027 100644
--- a/src/core/net/tcp6.cpp
+++ b/src/core/net/tcp6.cpp
@@ -43,6 +43,7 @@
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 #include "net/checksum.hpp"
 #include "net/ip6.hpp"
@@ -71,8 +72,8 @@
 
 Tcp::Tcp(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mTimer(aInstance, Tcp::HandleTimer)
-    , mTasklet(aInstance, Tcp::HandleTasklet)
+    , mTimer(aInstance)
+    , mTasklet(aInstance)
     , mEphemeralPort(kDynamicPortMin)
 {
     OT_UNUSED_VARIABLE(mEphemeralPort);
@@ -123,10 +124,7 @@
     return error;
 }
 
-Instance &Tcp::Endpoint::GetInstance(void) const
-{
-    return AsNonConst(AsCoreType(GetTcb().instance));
-}
+Instance &Tcp::Endpoint::GetInstance(void) const { return AsNonConst(AsCoreType(GetTcb().instance)); }
 
 const SockAddr &Tcp::Endpoint::GetLocalAddress(void) const
 {
@@ -171,7 +169,7 @@
 Error Tcp::Endpoint::Connect(const SockAddr &aSockName, uint32_t aFlags)
 {
     Error               error = kErrorNone;
-    struct tcpcb &      tp    = GetTcb();
+    struct tcpcb       &tp    = GetTcb();
     struct sockaddr_in6 sin6p;
 
     OT_UNUSED_VARIABLE(aFlags);
@@ -233,7 +231,11 @@
 
 Error Tcp::Endpoint::ReceiveContiguify(void)
 {
-    return kErrorNotImplemented;
+    struct tcpcb &tp = GetTcb();
+
+    cbuf_contiguify(&tp.recvbuf, tp.reassbmp);
+
+    return kErrorNone;
 }
 
 Error Tcp::Endpoint::CommitReceive(size_t aNumBytes, uint32_t aFlags)
@@ -282,10 +284,7 @@
     return error;
 }
 
-bool Tcp::Endpoint::IsClosed(void) const
-{
-    return GetTcb().t_state == TCP6S_CLOSED;
-}
+bool Tcp::Endpoint::IsClosed(void) const { return GetTcb().t_state == TCP6S_CLOSED; }
 
 uint8_t Tcp::Endpoint::TimerFlagToIndex(uint8_t aTimerFlag)
 {
@@ -431,7 +430,7 @@
             else
             {
                 aHasFutureTimer       = true;
-                aEarliestFutureExpiry = OT_MIN(aEarliestFutureExpiry, expiry);
+                aEarliestFutureExpiry = Min(aEarliestFutureExpiry, expiry);
             }
         }
     }
@@ -478,25 +477,16 @@
     return tp.snd_max - tp.snd_una;
 }
 
-size_t Tcp::Endpoint::GetBacklogBytes(void) const
-{
-    return GetSendBufferBytes() - GetInFlightBytes();
-}
+size_t Tcp::Endpoint::GetBacklogBytes(void) const { return GetSendBufferBytes() - GetInFlightBytes(); }
 
-Address &Tcp::Endpoint::GetLocalIp6Address(void)
-{
-    return *reinterpret_cast<Address *>(&GetTcb().laddr);
-}
+Address &Tcp::Endpoint::GetLocalIp6Address(void) { return *reinterpret_cast<Address *>(&GetTcb().laddr); }
 
 const Address &Tcp::Endpoint::GetLocalIp6Address(void) const
 {
     return *reinterpret_cast<const Address *>(&GetTcb().laddr);
 }
 
-Address &Tcp::Endpoint::GetForeignIp6Address(void)
-{
-    return *reinterpret_cast<Address *>(&GetTcb().faddr);
-}
+Address &Tcp::Endpoint::GetForeignIp6Address(void) { return *reinterpret_cast<Address *>(&GetTcb().faddr); }
 
 const Address &Tcp::Endpoint::GetForeignIp6Address(void) const
 {
@@ -538,10 +528,7 @@
     return error;
 }
 
-Instance &Tcp::Listener::GetInstance(void) const
-{
-    return AsNonConst(AsCoreType(GetTcbListen().instance));
-}
+Instance &Tcp::Listener::GetInstance(void) const { return AsNonConst(AsCoreType(GetTcbListen().instance)); }
 
 Error Tcp::Listener::Listen(const SockAddr &aSockName)
 {
@@ -581,15 +568,9 @@
     return error;
 }
 
-bool Tcp::Listener::IsClosed(void) const
-{
-    return GetTcbListen().t_state == TCP6S_CLOSED;
-}
+bool Tcp::Listener::IsClosed(void) const { return GetTcbListen().t_state == TCP6S_CLOSED; }
 
-Address &Tcp::Listener::GetLocalIp6Address(void)
-{
-    return *reinterpret_cast<Address *>(&GetTcbListen().laddr);
-}
+Address &Tcp::Listener::GetLocalIp6Address(void) { return *reinterpret_cast<Address *>(&GetTcbListen().laddr); }
 
 const Address &Tcp::Listener::GetLocalIp6Address(void) const
 {
@@ -625,7 +606,7 @@
     uint8_t  headerSize;
 
     struct ip6_hdr *ip6Header;
-    struct tcphdr * tcpHeader;
+    struct tcphdr  *tcpHeader;
 
     Endpoint *endpoint;
     Endpoint *endpointPrev;
@@ -655,7 +636,7 @@
     {
         struct tcplp_signals sig;
         int                  nextAction;
-        struct tcpcb *       tp = &endpoint->GetTcb();
+        struct tcpcb        *tp = &endpoint->GetTcb();
 
         otLinkedBuffer *priorHead    = lbuf_head(&tp->sendbuf);
         size_t          priorBacklog = endpoint->GetSendBufferBytes() - endpoint->GetInFlightBytes();
@@ -686,10 +667,10 @@
     return error;
 }
 
-void Tcp::ProcessSignals(Endpoint &            aEndpoint,
-                         otLinkedBuffer *      aPriorHead,
+void Tcp::ProcessSignals(Endpoint             &aEndpoint,
+                         otLinkedBuffer       *aPriorHead,
                          size_t                aPriorBacklog,
-                         struct tcplp_signals &aSignals)
+                         struct tcplp_signals &aSignals) const
 {
     VerifyOrExit(IsInitialized(aEndpoint) && !aEndpoint.IsClosed());
     if (aSignals.conn_established && aEndpoint.mEstablishedCallback != nullptr)
@@ -799,14 +780,11 @@
 
     if (aBindAddress)
     {
-        MessageInfo                  peerInfo;
-        const Netif::UnicastAddress *netifAddress;
+        const Address *source;
 
-        peerInfo.Clear();
-        peerInfo.SetPeerAddr(aPeer.GetAddress());
-        netifAddress = Get<Ip6>().SelectSourceAddress(peerInfo);
-        VerifyOrExit(netifAddress != nullptr, success = false);
-        aToBind.GetAddress() = netifAddress->GetAddress();
+        source = Get<Ip6>().SelectSourceAddress(aPeer.GetAddress());
+        VerifyOrExit(source != nullptr, success = false);
+        aToBind.SetAddress(*source);
     }
 
     if (aBindPort)
@@ -844,20 +822,13 @@
     return success;
 }
 
-void Tcp::HandleTimer(Timer &aTimer)
-{
-    OT_ASSERT(&aTimer == &aTimer.Get<Tcp>().mTimer);
-    LogDebg("Main TCP timer expired");
-    aTimer.Get<Tcp>().ProcessTimers();
-}
-
-void Tcp::ProcessTimers(void)
+void Tcp::HandleTimer(void)
 {
     TimeMilli now = TimerMilli::GetNow();
     bool      pendingTimer;
     TimeMilli earliestPendingTimerExpiry;
 
-    OT_ASSERT(!mTimer.IsRunning());
+    LogDebg("Main TCP timer expired");
 
     /*
      * The timer callbacks could potentially set/reset/cancel timers.
@@ -913,13 +884,6 @@
     }
 }
 
-void Tcp::HandleTasklet(Tasklet &aTasklet)
-{
-    OT_ASSERT(&aTasklet == &aTasklet.Get<Tcp>().mTasklet);
-    LogDebg("TCP tasklet invoked");
-    aTasklet.Get<Tcp>().ProcessCallbacks();
-}
-
 void Tcp::ProcessCallbacks(void)
 {
     for (Endpoint &endpoint : mEndpoints)
@@ -953,7 +917,7 @@
 otMessage *tcplp_sys_new_message(otInstance *aInstance)
 {
     Instance &instance = AsCoreType(aInstance);
-    Message * message  = instance.Get<ot::Ip6::Ip6>().NewMessage(0);
+    Message  *message  = instance.Get<ot::Ip6::Ip6>().NewMessage(0);
 
     if (message)
     {
@@ -972,8 +936,8 @@
 
 void tcplp_sys_send_message(otInstance *aInstance, otMessage *aMessage, otMessageInfo *aMessageInfo)
 {
-    Instance &   instance = AsCoreType(aInstance);
-    Message &    message  = AsCoreType(aMessage);
+    Instance    &instance = AsCoreType(aInstance);
+    Message     &message  = AsCoreType(aMessage);
     MessageInfo &info     = AsCoreType(aMessageInfo);
 
     LogDebg("Sending TCP segment: payload_size = %d", static_cast<int>(message.GetLength()));
@@ -981,15 +945,9 @@
     IgnoreError(instance.Get<ot::Ip6::Ip6>().SendDatagram(message, info, kProtoTcp));
 }
 
-uint32_t tcplp_sys_get_ticks(void)
-{
-    return TimerMilli::GetNow().GetValue();
-}
+uint32_t tcplp_sys_get_ticks(void) { return TimerMilli::GetNow().GetValue(); }
 
-uint32_t tcplp_sys_get_millis(void)
-{
-    return TimerMilli::GetNow().GetValue();
-}
+uint32_t tcplp_sys_get_millis(void) { return TimerMilli::GetNow().GetValue(); }
 
 void tcplp_sys_set_timer(struct tcpcb *aTcb, uint8_t aTimerFlag, uint32_t aDelay)
 {
@@ -1005,11 +963,11 @@
 
 struct tcpcb *tcplp_sys_accept_ready(struct tcpcb_listen *aTcbListen, struct in6_addr *aAddr, uint16_t aPort)
 {
-    Tcp::Listener &               listener = Tcp::Listener::FromTcbListen(*aTcbListen);
-    Tcp &                         tcp      = listener.Get<Tcp>();
-    struct tcpcb *                rv       = (struct tcpcb *)-1;
+    Tcp::Listener                &listener = Tcp::Listener::FromTcbListen(*aTcbListen);
+    Tcp                          &tcp      = listener.Get<Tcp>();
+    struct tcpcb                 *rv       = (struct tcpcb *)-1;
     otSockAddr                    addr;
-    otTcpEndpoint *               endpointPtr;
+    otTcpEndpoint                *endpointPtr;
     otTcpIncomingConnectionAction action;
 
     VerifyOrExit(listener.mAcceptReadyCallback != nullptr);
@@ -1051,13 +1009,13 @@
 }
 
 bool tcplp_sys_accepted_connection(struct tcpcb_listen *aTcbListen,
-                                   struct tcpcb *       aAccepted,
-                                   struct in6_addr *    aAddr,
+                                   struct tcpcb        *aAccepted,
+                                   struct in6_addr     *aAddr,
                                    uint16_t             aPort)
 {
     Tcp::Listener &listener = Tcp::Listener::FromTcbListen(*aTcbListen);
     Tcp::Endpoint &endpoint = Tcp::Endpoint::FromTcb(*aAccepted);
-    Tcp &          tcp      = endpoint.Get<Tcp>();
+    Tcp           &tcp      = endpoint.Get<Tcp>();
     bool           accepted = true;
 
     if (listener.mAcceptDoneCallback != nullptr)
@@ -1125,7 +1083,7 @@
     vsnprintf(buffer, sizeof(buffer), aFormat, args);
     va_end(args);
 
-    LogDebg(buffer);
+    LogDebg("%s", buffer);
 }
 
 void tcplp_sys_panic(const char *aFormat, ...)
@@ -1141,9 +1099,9 @@
     OT_ASSERT(false);
 }
 
-bool tcplp_sys_autobind(otInstance *      aInstance,
+bool tcplp_sys_autobind(otInstance       *aInstance,
                         const otSockAddr *aPeer,
-                        otSockAddr *      aToBind,
+                        otSockAddr       *aToBind,
                         bool              aBindAddress,
                         bool              aBindPort)
 {
@@ -1160,15 +1118,9 @@
     return isn;
 }
 
-uint16_t tcplp_sys_hostswap16(uint16_t aHostPort)
-{
-    return HostSwap16(aHostPort);
-}
+uint16_t tcplp_sys_hostswap16(uint16_t aHostPort) { return HostSwap16(aHostPort); }
 
-uint32_t tcplp_sys_hostswap32(uint32_t aHostPort)
-{
-    return HostSwap32(aHostPort);
-}
+uint32_t tcplp_sys_hostswap32(uint32_t aHostPort) { return HostSwap32(aHostPort); }
 }
 
 #endif // OPENTHREAD_CONFIG_TCP_ENABLE
diff --git a/src/core/net/tcp6.hpp b/src/core/net/tcp6.hpp
index 501fd5a..ec407de 100644
--- a/src/core/net/tcp6.hpp
+++ b/src/core/net/tcp6.hpp
@@ -55,7 +55,16 @@
 struct tcpcb;
 struct tcpcb_listen;
 struct tcplp_signals;
+
+/*
+ * The next two declarations intentionally change argument names from the
+ * original declarations in TCPlp, in order to comply with OpenThread's format.
+ */
+
+// NOLINTNEXTLINE(readability-inconsistent-declaration-parameter-name)
 void tcplp_sys_set_timer(struct tcpcb *aTcb, uint8_t aTimerFlag, uint32_t aDelay);
+
+// NOLINTNEXTLINE(readability-inconsistent-declaration-parameter-name)
 void tcplp_sys_stop_timer(struct tcpcb *aTcb, uint8_t aTimerFlag);
 }
 
@@ -391,9 +400,9 @@
         size_t GetInFlightBytes(void) const;
         size_t GetBacklogBytes(void) const;
 
-        Address &      GetLocalIp6Address(void);
+        Address       &GetLocalIp6Address(void);
         const Address &GetLocalIp6Address(void) const;
-        Address &      GetForeignIp6Address(void);
+        Address       &GetForeignIp6Address(void);
         const Address &GetForeignIp6Address(void) const;
         bool           Matches(const MessageInfo &aMessageInfo) const;
     };
@@ -520,7 +529,7 @@
         bool IsClosed(void) const;
 
     private:
-        Address &      GetLocalIp6Address(void);
+        Address       &GetLocalIp6Address(void);
         const Address &GetLocalIp6Address(void) const;
         bool           Matches(const MessageInfo &aMessageInfo) const;
     };
@@ -669,22 +678,23 @@
     static constexpr uint8_t kReceiveAvailableCallbackFlag = (1 << 3);
     static constexpr uint8_t kDisconnectedCallbackFlag     = (1 << 4);
 
-    void ProcessSignals(Endpoint &            aEndpoint,
-                        otLinkedBuffer *      aPriorHead,
+    void ProcessSignals(Endpoint             &aEndpoint,
+                        otLinkedBuffer       *aPriorHead,
                         size_t                aPriorBacklog,
-                        struct tcplp_signals &aSignals);
+                        struct tcplp_signals &aSignals) const;
 
     static Error BsdErrorToOtError(int aBsdError);
     bool         CanBind(const SockAddr &aSockName);
 
-    static void HandleTimer(Timer &aTimer);
-    void        ProcessTimers(void);
+    void HandleTimer(void);
 
-    static void HandleTasklet(Tasklet &aTasklet);
-    void        ProcessCallbacks(void);
+    void ProcessCallbacks(void);
 
-    TimerMilli mTimer;
-    Tasklet    mTasklet;
+    using TcpTasklet = TaskletIn<Tcp, &Tcp::ProcessCallbacks>;
+    using TcpTimer   = TimerMilliIn<Tcp, &Tcp::HandleTimer>;
+
+    TcpTimer   mTimer;
+    TcpTasklet mTasklet;
 
     LinkedList<Endpoint> mEndpoints;
     LinkedList<Listener> mListeners;
diff --git a/src/core/net/tcp6_ext.cpp b/src/core/net/tcp6_ext.cpp
new file mode 100644
index 0000000..c7601e8
--- /dev/null
+++ b/src/core/net/tcp6_ext.cpp
@@ -0,0 +1,225 @@
+/*
+ *  Copyright (c) 2022, 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 TCP/IPv6 socket extensions.
+ */
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_TCP_ENABLE
+
+#include "tcp6_ext.hpp"
+
+#include "common/code_utils.hpp"
+#include "common/error.hpp"
+#include "common/locator_getters.hpp"
+#include "common/log.hpp"
+
+namespace ot {
+namespace Ip6 {
+
+RegisterLogModule("TcpExt");
+
+void TcpCircularSendBuffer::Initialize(void *aDataBuffer, size_t aCapacity)
+{
+    mDataBuffer = static_cast<uint8_t *>(aDataBuffer);
+    mCapacity   = aCapacity;
+    ForceDiscardAll();
+}
+
+Error TcpCircularSendBuffer::Write(Tcp::Endpoint &aEndpoint,
+                                   const void    *aData,
+                                   size_t         aLength,
+                                   size_t        &aWritten,
+                                   uint32_t       aFlags)
+{
+    Error    error     = kErrorNone;
+    size_t   bytesFree = GetFreeSpace();
+    size_t   writeIndex;
+    uint32_t flags = 0;
+    size_t   bytesUntilWrap;
+
+    /*
+     * Handle the case where we don't have enough space to accommodate all of the
+     * provided data.
+     */
+    aLength = Min(aLength, bytesFree);
+    VerifyOrExit(aLength != 0);
+
+    /*
+     * This is a "simplifying" if statement the removes an edge case from the logic
+     * below. It guarantees that a write to an empty buffer will never wrap.
+     */
+    if (mCapacityUsed == 0)
+    {
+        mStartIndex = 0;
+    }
+
+    writeIndex = GetIndex(mStartIndex, mCapacityUsed);
+
+    if ((aFlags & OT_TCP_CIRCULAR_SEND_BUFFER_WRITE_MORE_TO_COME) != 0 && aLength < bytesFree)
+    {
+        flags |= OT_TCP_SEND_MORE_TO_COME;
+    }
+
+    bytesUntilWrap = mCapacity - writeIndex;
+    if (aLength <= bytesUntilWrap)
+    {
+        memcpy(&mDataBuffer[writeIndex], aData, aLength);
+        if (writeIndex == 0)
+        {
+            /*
+             * mCapacityUsed == 0 corresponds to the case where we're writing
+             * to an empty buffer. mCapacityUsed != 0 && writeIndex == 0
+             * corresponds to the case where the buffer is not empty and this is
+             * writing the first bytes that wrap.
+             */
+            uint8_t linkIndex;
+            if (mCapacityUsed == 0)
+            {
+                linkIndex = mFirstSendLinkIndex;
+            }
+            else
+            {
+                linkIndex = 1 - mFirstSendLinkIndex;
+            }
+            {
+                otLinkedBuffer &dataSendLink = mSendLinks[linkIndex];
+
+                dataSendLink.mNext   = nullptr;
+                dataSendLink.mData   = &mDataBuffer[writeIndex];
+                dataSendLink.mLength = aLength;
+
+                LogDebg("Appending link %u (points to index %u, length %u)", static_cast<unsigned>(linkIndex),
+                        static_cast<unsigned>(writeIndex), static_cast<unsigned>(aLength));
+                error = aEndpoint.SendByReference(dataSendLink, flags);
+            }
+        }
+        else
+        {
+            LogDebg("Extending tail link by length %u", static_cast<unsigned>(aLength));
+            error = aEndpoint.SendByExtension(aLength, flags);
+        }
+        VerifyOrExit(error == kErrorNone, aLength = 0);
+    }
+    else
+    {
+        const uint8_t *dataIndexable = static_cast<const uint8_t *>(aData);
+        size_t         bytesWrapped  = aLength - bytesUntilWrap;
+
+        memcpy(&mDataBuffer[writeIndex], &dataIndexable[0], bytesUntilWrap);
+        memcpy(&mDataBuffer[0], &dataIndexable[bytesUntilWrap], bytesWrapped);
+
+        /*
+         * Because of the "simplifying" if statement at the top, we don't
+         * have to worry about starting from an empty buffer in this case.
+         */
+        LogDebg("Extending tail link by length %u (wrapping)", static_cast<unsigned>(bytesUntilWrap));
+        error = aEndpoint.SendByExtension(bytesUntilWrap, flags | OT_TCP_SEND_MORE_TO_COME);
+        VerifyOrExit(error == kErrorNone, aLength = 0);
+
+        {
+            otLinkedBuffer &wrappedDataSendLink = mSendLinks[1 - mFirstSendLinkIndex];
+
+            wrappedDataSendLink.mNext   = nullptr;
+            wrappedDataSendLink.mData   = &mDataBuffer[0];
+            wrappedDataSendLink.mLength = bytesWrapped;
+
+            LogDebg("Appending link %u (wrapping)", static_cast<unsigned>(1 - mFirstSendLinkIndex));
+            error = aEndpoint.SendByReference(wrappedDataSendLink, flags);
+            VerifyOrExit(error == kErrorNone, aLength = bytesUntilWrap);
+        }
+    }
+
+exit:
+    mCapacityUsed += aLength;
+    aWritten = aLength;
+    return error;
+}
+
+void TcpCircularSendBuffer::HandleForwardProgress(size_t aInSendBuffer)
+{
+    size_t bytesRemoved;
+    size_t bytesUntilWrap;
+
+    OT_ASSERT(aInSendBuffer <= mCapacityUsed);
+    LogDebg("Forward progress: %u bytes in send buffer\n", static_cast<unsigned>(aInSendBuffer));
+    bytesRemoved   = mCapacityUsed - aInSendBuffer;
+    bytesUntilWrap = mCapacity - mStartIndex;
+
+    if (bytesRemoved < bytesUntilWrap)
+    {
+        mStartIndex += bytesRemoved;
+    }
+    else
+    {
+        mStartIndex = bytesRemoved - bytesUntilWrap;
+        /* The otLinkedBuffer for the pre-wrap data is now empty. */
+        LogDebg("Pre-wrap linked buffer now empty: switching first link index from %u to %u\n",
+                static_cast<unsigned>(mFirstSendLinkIndex), static_cast<unsigned>(1 - mFirstSendLinkIndex));
+        mFirstSendLinkIndex = 1 - mFirstSendLinkIndex;
+    }
+    mCapacityUsed = aInSendBuffer;
+}
+
+size_t TcpCircularSendBuffer::GetFreeSpace(void) const { return mCapacity - mCapacityUsed; }
+
+void TcpCircularSendBuffer::ForceDiscardAll(void)
+{
+    mStartIndex         = 0;
+    mCapacityUsed       = 0;
+    mFirstSendLinkIndex = 0;
+}
+
+Error TcpCircularSendBuffer::Deinitialize(void) { return (mCapacityUsed != 0) ? kErrorBusy : kErrorNone; }
+
+size_t TcpCircularSendBuffer::GetIndex(size_t aStart, size_t aOffsetFromStart) const
+{
+    size_t bytesUntilWrap;
+    size_t index;
+
+    OT_ASSERT(aStart < mCapacity);
+    bytesUntilWrap = mCapacity - aStart;
+    if (aOffsetFromStart < bytesUntilWrap)
+    {
+        index = aStart + aOffsetFromStart;
+    }
+    else
+    {
+        index = aOffsetFromStart - bytesUntilWrap;
+    }
+
+    return index;
+}
+
+} // namespace Ip6
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_TCP_ENABLE
diff --git a/src/core/net/tcp6_ext.hpp b/src/core/net/tcp6_ext.hpp
new file mode 100644
index 0000000..b40663a
--- /dev/null
+++ b/src/core/net/tcp6_ext.hpp
@@ -0,0 +1,136 @@
+/*
+ *  Copyright (c) 2022, 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 definitions for TCP/IPv6 socket extensions.
+ */
+
+#ifndef TCP6_EXT_HPP_
+#define TCP6_EXT_HPP_
+
+#include "openthread-core-config.h"
+
+#include <openthread/tcp_ext.h>
+
+#include "net/tcp6.hpp"
+
+namespace ot {
+namespace Ip6 {
+
+/**
+ * @addtogroup core-tcp-ext
+ *
+ * @brief
+ *   This module includes definitions for TCP/IPv6 socket extensions.
+ *
+ * @{
+ *
+ */
+
+/**
+ * This class represents a TCP circular send buffer.
+ *
+ */
+class TcpCircularSendBuffer : public otTcpCircularSendBuffer
+{
+public:
+    /**
+     * Initializes a TCP circular send buffer.
+     *
+     * @sa otTcpCircularSendBufferInitialize
+     *
+     * @param[in]  aDataBuffer      A pointer to memory to use to store data in the TCP circular send buffer.
+     * @param[in]  aCapacity        The capacity, in bytes, of the TCP circular send buffer, which must equal the size
+     *                              of the memory pointed to by @p aDataBuffer .
+     */
+    void Initialize(void *aDataBuffer, size_t aCapacity);
+
+    /**
+     * Sends out data on a TCP endpoint, using this TCP circular send buffer to manage buffering.
+     *
+     * @sa otTcpCircularSendBufferWrite, particularly for guidance on how @p aEndpoint must be chosen.
+     *
+     * @param[in]   aEndpoint The TCP endpoint on which to send out data.
+     * @param[in]   aData     A pointer to data to copy into the TCP circular send buffer.
+     * @param[in]   aLength   The length of the data pointed to by @p aData to copy into the TCP circular send buffer.
+     * @param[out]  aWritten  Populated with the amount of data copied into the send buffer, which might be less than
+     *                        @p aLength if the send buffer reaches capacity.
+     * @param[in]   aFlags    Flags specifying options for this operation.
+     *
+     * @retval kErrorNone on success, or the error returned by the TCP endpoint on failure.
+     */
+    Error Write(Tcp::Endpoint &aEndpoint, const void *aData, size_t aLength, size_t &aWritten, uint32_t aFlags);
+
+    /**
+     * Performs circular-send-buffer-specific handling in the otTcpForwardProgress callback.
+     *
+     * @sa otTcpCircularSendBufferHandleForwardProgress
+     *
+     * @param[in]  aInSendBuffer  Value of @p aInSendBuffer passed to the otTcpForwardProgress() callback.
+     */
+    void HandleForwardProgress(size_t aInSendBuffer);
+
+    /**
+     * Returns the amount of free space in this TCP circular send buffer.
+     *
+     * @sa otTcpCircularSendBufferFreeSpace
+     *
+     * @return The amount of free space in the send buffer.
+     */
+    size_t GetFreeSpace(void) const;
+
+    /**
+     * Forcibly discards all data in this TCP circular send buffer.
+     *
+     * @sa otTcpCircularSendBufferForceDiscardAll
+     *
+     */
+    void ForceDiscardAll(void);
+
+    /**
+     * Deinitializes this TCP circular send buffer.
+     *
+     * @sa otTcpCircularSendBufferDeinitialize
+     *
+     * @retval kErrorNone    Successfully deinitialized this TCP circular send buffer.
+     * @retval kErrorFailed  Failed to deinitialize the TCP circular send buffer.
+     */
+    Error Deinitialize(void);
+
+private:
+    size_t GetIndex(size_t aStart, size_t aOffsetFromStart) const;
+};
+
+} // namespace Ip6
+
+DefineCoreType(otTcpCircularSendBuffer, Ip6::TcpCircularSendBuffer);
+
+} // namespace ot
+
+#endif // TCP6_HPP_
diff --git a/src/core/net/udp6.cpp b/src/core/net/udp6.cpp
index 1dea10e..3ddda61 100644
--- a/src/core/net/udp6.cpp
+++ b/src/core/net/udp6.cpp
@@ -77,45 +77,34 @@
     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);
 }
 
-Error Udp::Socket::Open(otUdpReceive aHandler, void *aContext)
-{
-    return Get<Udp>().Open(*this, aHandler, aContext);
-}
+Error Udp::Socket::Open(otUdpReceive aHandler, void *aContext) { return Get<Udp>().Open(*this, aHandler, aContext); }
 
-bool Udp::Socket::IsOpen(void) const
-{
-    return Get<Udp>().IsOpen(*this);
-}
+bool Udp::Socket::IsOpen(void) const { return Get<Udp>().IsOpen(*this); }
 
-Error Udp::Socket::Bind(const SockAddr &aSockAddr, otNetifIdentifier aNetifIdentifier)
+Error Udp::Socket::Bind(const SockAddr &aSockAddr, NetifIdentifier aNetifIdentifier)
 {
     return Get<Udp>().Bind(*this, aSockAddr, aNetifIdentifier);
 }
 
-Error Udp::Socket::Bind(uint16_t aPort, otNetifIdentifier aNetifIdentifier)
+Error Udp::Socket::Bind(uint16_t aPort, NetifIdentifier aNetifIdentifier)
 {
     return Bind(SockAddr(aPort), aNetifIdentifier);
 }
 
-Error Udp::Socket::Connect(const SockAddr &aSockAddr)
-{
-    return Get<Udp>().Connect(*this, aSockAddr);
-}
+Error Udp::Socket::Connect(const SockAddr &aSockAddr) { return Get<Udp>().Connect(*this, aSockAddr); }
 
-Error Udp::Socket::Connect(uint16_t aPort)
-{
-    return Connect(SockAddr(aPort));
-}
+Error Udp::Socket::Connect(uint16_t aPort) { return Connect(SockAddr(aPort)); }
 
-Error Udp::Socket::Close(void)
-{
-    return Get<Udp>().Close(*this);
-}
+Error Udp::Socket::Close(void) { return Get<Udp>().Close(*this); }
 
 Error Udp::Socket::SendTo(Message &aMessage, const MessageInfo &aMessageInfo)
 {
@@ -123,7 +112,7 @@
 }
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
-Error Udp::Socket::JoinNetifMulticastGroup(otNetifIdentifier aNetifIdentifier, const Address &aAddress)
+Error Udp::Socket::JoinNetifMulticastGroup(NetifIdentifier aNetifIdentifier, const Address &aAddress)
 {
     OT_UNUSED_VARIABLE(aNetifIdentifier);
     OT_UNUSED_VARIABLE(aAddress);
@@ -133,14 +122,14 @@
     VerifyOrExit(aAddress.IsMulticast(), error = kErrorInvalidArgs);
 
 #if OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE
-    error = otPlatUdpJoinMulticastGroup(this, aNetifIdentifier, &aAddress);
+    error = otPlatUdpJoinMulticastGroup(this, MapEnum(aNetifIdentifier), &aAddress);
 #endif
 
 exit:
     return error;
 }
 
-Error Udp::Socket::LeaveNetifMulticastGroup(otNetifIdentifier aNetifIdentifier, const Address &aAddress)
+Error Udp::Socket::LeaveNetifMulticastGroup(NetifIdentifier aNetifIdentifier, const Address &aAddress)
 {
     OT_UNUSED_VARIABLE(aNetifIdentifier);
     OT_UNUSED_VARIABLE(aAddress);
@@ -150,7 +139,7 @@
     VerifyOrExit(aAddress.IsMulticast(), error = kErrorInvalidArgs);
 
 #if OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE
-    error = otPlatUdpLeaveMulticastGroup(this, aNetifIdentifier, &aAddress);
+    error = otPlatUdpLeaveMulticastGroup(this, MapEnum(aNetifIdentifier), &aAddress);
 #endif
 
 exit:
@@ -164,17 +153,10 @@
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
     , mPrevBackboneSockets(nullptr)
 #endif
-#if OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE
-    , mUdpForwarderContext(nullptr)
-    , mUdpForwarder(nullptr)
-#endif
 {
 }
 
-Error Udp::AddReceiver(Receiver &aReceiver)
-{
-    return mReceivers.Add(aReceiver);
-}
+Error Udp::AddReceiver(Receiver &aReceiver) { return mReceivers.Add(aReceiver); }
 
 Error Udp::RemoveReceiver(Receiver &aReceiver)
 {
@@ -209,18 +191,18 @@
     return error;
 }
 
-Error Udp::Bind(SocketHandle &aSocket, const SockAddr &aSockAddr, otNetifIdentifier aNetifIdentifier)
+Error Udp::Bind(SocketHandle &aSocket, const SockAddr &aSockAddr, NetifIdentifier aNetifIdentifier)
 {
     OT_UNUSED_VARIABLE(aNetifIdentifier);
 
     Error error = kErrorNone;
 
 #if OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE
-    SuccessOrExit(error = otPlatUdpBindToNetif(&aSocket, aNetifIdentifier));
+    SuccessOrExit(error = otPlatUdpBindToNetif(&aSocket, MapEnum(aNetifIdentifier)));
 #endif
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
-    if (aNetifIdentifier == OT_NETIF_BACKBONE)
+    if (aNetifIdentifier == kNetifBackbone)
     {
         SetBackboneSocket(aSocket);
     }
@@ -297,7 +279,7 @@
 
     if (!aSocket.IsBound())
     {
-        SuccessOrExit(error = Bind(aSocket, aSocket.GetSockName(), OT_NETIF_THREAD));
+        SuccessOrExit(error = Bind(aSocket, aSocket.GetSockName(), kNetifThread));
     }
 
 #if OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE
@@ -360,7 +342,7 @@
 
     if (!aSocket.IsBound())
     {
-        SuccessOrExit(error = Bind(aSocket, aSocket.GetSockName(), OT_NETIF_THREAD));
+        SuccessOrExit(error = Bind(aSocket, aSocket.GetSockName(), kNetifThread));
     }
 
     messageInfoLocal.SetSockPort(aSocket.GetSockName().mPort);
@@ -436,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);
@@ -448,9 +434,8 @@
 #if OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE
     if (aMessageInfo.IsHostInterface())
     {
-        VerifyOrExit(mUdpForwarder != nullptr, error = kErrorNoRoute);
-        mUdpForwarder(&aMessage, aMessageInfo.mPeerPort, &aMessageInfo.GetPeerAddr(), aMessageInfo.mSockPort,
-                      mUdpForwarderContext);
+        VerifyOrExit(mUdpForwarder.IsSet(), error = kErrorNoRoute);
+        mUdpForwarder.Invoke(&aMessage, aMessageInfo.mPeerPort, &aMessageInfo.GetPeerAddr(), aMessageInfo.mSockPort);
         // message is consumed by the callback
     }
     else
diff --git a/src/core/net/udp6.hpp b/src/core/net/udp6.hpp
index 6471c4b..6648f00 100644
--- a/src/core/net/udp6.hpp
+++ b/src/core/net/udp6.hpp
@@ -40,6 +40,7 @@
 #include <openthread/platform/udp.h>
 
 #include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/clearable.hpp"
 #include "common/linked_list.hpp"
 #include "common/locator.hpp"
@@ -66,6 +67,17 @@
 #endif
 
 /**
+ * This enumeration defines the network interface identifiers.
+ *
+ */
+enum NetifIdentifier : uint8_t
+{
+    kNetifUnspecified = OT_NETIF_UNSPECIFIED, ///< Unspecified network interface.
+    kNetifThread      = OT_NETIF_THREAD,      ///< The Thread interface.
+    kNetifBackbone    = OT_NETIF_BACKBONE,    ///< The Backbone interface.
+};
+
+/**
  * This class implements core UDP message handling.
  *
  */
@@ -150,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.
@@ -158,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.
@@ -191,7 +221,7 @@
          * @retval kErrorFailed          Failed to bind UDP Socket.
          *
          */
-        Error Bind(const SockAddr &aSockAddr, otNetifIdentifier aNetifIdentifier = OT_NETIF_THREAD);
+        Error Bind(const SockAddr &aSockAddr, NetifIdentifier aNetifIdentifier = kNetifThread);
 
         /**
          * This method binds the UDP socket.
@@ -203,7 +233,7 @@
          * @retval kErrorFailed          Failed to bind UDP Socket.
          *
          */
-        Error Bind(uint16_t aPort, otNetifIdentifier aNetifIdentifier = OT_NETIF_THREAD);
+        Error Bind(uint16_t aPort, NetifIdentifier aNetifIdentifier = kNetifThread);
 
         /**
          * This method binds the UDP socket.
@@ -269,7 +299,7 @@
 
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
         /**
-         * This method configures the UDP socket to join a mutlicast group on a Host network interface.
+         * This method configures the UDP socket to join a multicast group on a Host network interface.
          *
          * @param[in]  aNetifIdentifier     The network interface identifier.
          * @param[in]  aAddress             The multicast group address.
@@ -278,7 +308,7 @@
          * @retval  kErrorFailed  Failed to join the multicast group.
          *
          */
-        Error JoinNetifMulticastGroup(otNetifIdentifier aNetifIdentifier, const Address &aAddress);
+        Error JoinNetifMulticastGroup(NetifIdentifier aNetifIdentifier, const Address &aAddress);
 
         /**
          * This method configures the UDP socket to leave a multicast group on a Host network interface.
@@ -290,7 +320,7 @@
          * @retval  kErrorFailed Failed to leave the multicast group.
          *
          */
-        Error LeaveNetifMulticastGroup(otNetifIdentifier aNetifIdentifier, const Address &aAddress);
+        Error LeaveNetifMulticastGroup(NetifIdentifier aNetifIdentifier, const Address &aAddress);
 #endif
     };
 
@@ -474,7 +504,7 @@
      * @retval kErrorFailed          Failed to bind UDP Socket.
      *
      */
-    Error Bind(SocketHandle &aSocket, const SockAddr &aSockAddr, otNetifIdentifier aNetifIdentifier);
+    Error Bind(SocketHandle &aSocket, const SockAddr &aSockAddr, NetifIdentifier aNetifIdentifier);
 
     /**
      * This method connects a UDP socket.
@@ -522,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.
@@ -530,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.
@@ -582,11 +630,7 @@
      * @param[in]   aContext    A pointer to arbitrary context information.
      *
      */
-    void SetUdpForwarder(otUdpForwarder aForwarder, void *aContext)
-    {
-        mUdpForwarder        = aForwarder;
-        mUdpForwarderContext = aContext;
-    }
+    void SetUdpForwarder(otUdpForwarder aForwarder, void *aContext) { mUdpForwarder.Set(aForwarder, aContext); }
 #endif
 
     /**
@@ -641,8 +685,7 @@
     SocketHandle *mPrevBackboneSockets;
 #endif
 #if OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE
-    void *         mUdpForwarderContext;
-    otUdpForwarder mUdpForwarder;
+    Callback<otUdpForwarder> mUdpForwarder;
 #endif
 };
 
@@ -655,6 +698,7 @@
 
 DefineCoreType(otUdpSocket, Ip6::Udp::SocketHandle);
 DefineCoreType(otUdpReceiver, Ip6::Udp::Receiver);
+DefineMapEnum(otNetifIdentifier, Ip6::NetifIdentifier);
 
 } // namespace ot
 
diff --git a/src/core/openthread-core-config.h b/src/core/openthread-core-config.h
index da7e185..2b4127d 100644
--- a/src/core/openthread-core-config.h
+++ b/src/core/openthread-core-config.h
@@ -41,6 +41,7 @@
 #define OT_THREAD_VERSION_1_1 2
 #define OT_THREAD_VERSION_1_2 3
 #define OT_THREAD_VERSION_1_3 4
+#define OT_THREAD_VERSION_1_3_1 5
 
 #define OPENTHREAD_CORE_CONFIG_H_IN
 
@@ -58,7 +59,9 @@
 
 #include "config/announce_sender.h"
 #include "config/backbone_router.h"
+#include "config/border_agent.h"
 #include "config/border_router.h"
+#include "config/border_routing.h"
 #include "config/channel_manager.h"
 #include "config/channel_monitor.h"
 #include "config/child_supervision.h"
@@ -80,12 +83,16 @@
 #include "config/link_raw.h"
 #include "config/logging.h"
 #include "config/mac.h"
+#include "config/mesh_diag.h"
 #include "config/misc.h"
 #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"
+#include "config/power_calibration.h"
 #include "config/radio_link.h"
 #include "config/sntp_client.h"
 #include "config/srp_client.h"
diff --git a/src/core/radio.cmake b/src/core/radio.cmake
index f70a8dd..95ae915 100644
--- a/src/core/radio.cmake
+++ b/src/core/radio.cmake
@@ -54,5 +54,6 @@
 target_link_libraries(openthread-radio
     PRIVATE
         ${OT_MBEDTLS_RCP}
+        ot-config-radio
         ot-config
 )
diff --git a/src/core/radio/radio.hpp b/src/core/radio/radio.hpp
index aa7233e..bbe8544 100644
--- a/src/core/radio/radio.hpp
+++ b/src/core/radio/radio.hpp
@@ -45,7 +45,8 @@
 
 namespace ot {
 
-static constexpr uint32_t kUsPerTenSymbols = OT_US_PER_TEN_SYMBOLS; ///< The microseconds per 10 symbols.
+static constexpr uint32_t kUsPerTenSymbols = OT_US_PER_TEN_SYMBOLS; ///< Time for 10 symbols in units of microseconds
+static constexpr uint32_t kRadioHeaderShrDuration = 160;            ///< Duration of SHR in us
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 /**
@@ -56,12 +57,6 @@
 static constexpr uint64_t kMaxCslTimeout = OPENTHREAD_CONFIG_MAC_CSL_MAX_TIMEOUT;
 #endif
 
-#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-static constexpr uint8_t kCslWorstCrystalPpm  = 255; ///< Worst possible crystal accuracy, in units of ± ppm.
-static constexpr uint8_t kCslWorstUncertainty = 255; ///< Worst possible scheduling uncertainty, in units of 10 us.
-static constexpr uint8_t kUsPerUncertUnit     = 10;  ///< Number of microseconds by uncertainty unit.
-#endif
-
 /**
  * @addtogroup core-radio
  *
@@ -109,6 +104,10 @@
     static constexpr uint32_t kSupportedChannelPages = (1 << OPENTHREAD_CONFIG_PLATFORM_RADIO_PROPRIETARY_CHANNEL_PAGE);
 #endif
 
+    static constexpr int8_t kInvalidRssi = OT_RADIO_RSSI_INVALID; ///< Invalid RSSI value.
+
+    static constexpr int8_t kDefaultReceiveSensitivity = -110; ///< Default receive sensitivity (in dBm).
+
     static_assert((OPENTHREAD_CONFIG_RADIO_2P4GHZ_OQPSK_SUPPORT || OPENTHREAD_CONFIG_RADIO_915MHZ_OQPSK_SUPPORT ||
                    OPENTHREAD_CONFIG_PLATFORM_RADIO_PROPRIETARY_SUPPORT),
                   "OPENTHREAD_CONFIG_RADIO_2P4GHZ_OQPSK_SUPPORT "
@@ -242,7 +241,7 @@
      * @returns The radio receive sensitivity value in dBm.
      *
      */
-    int8_t GetReceiveSensitivity(void);
+    int8_t GetReceiveSensitivity(void) const;
 
 #if OPENTHREAD_RADIO
     /**
@@ -304,6 +303,18 @@
     }
 
     /**
+     * This method sets the current MAC Frame Counter value only if the new given value is larger than the current
+     * value.
+     *
+     * @param[in] aMacFrameCounter  The MAC Frame Counter value.
+     *
+     */
+    void SetMacFrameCounterIfLarger(uint32_t aMacFrameCounter)
+    {
+        otPlatRadioSetMacFrameCounterIfLarger(GetInstancePtr(), aMacFrameCounter);
+    }
+
+    /**
      * This method gets the radio's transmit power in dBm.
      *
      * @param[out] aPower    A reference to output the transmit power in dBm.
@@ -473,7 +484,7 @@
     uint8_t GetCslAccuracy(void);
 
     /**
-     * Get the fixed uncertainty of the Device for scheduling CSL Transmissions in units of 10 microseconds.
+     * Get the fixed uncertainty of the Device for scheduling CSL operations in units of 10 microseconds.
      *
      * @returns The CSL Uncertainty in units of 10 us.
      *
@@ -642,7 +653,7 @@
      */
     Error ConfigureEnhAckProbing(otLinkMetrics            aLinkMetrics,
                                  const Mac::ShortAddress &aShortAddress,
-                                 const Mac::ExtAddress &  aExtAddress)
+                                 const Mac::ExtAddress   &aExtAddress)
     {
         return otPlatRadioConfigureEnhAckProbing(GetInstancePtr(), aLinkMetrics, aShortAddress, &aExtAddress);
     }
@@ -662,7 +673,7 @@
     }
 
 private:
-    otInstance *GetInstancePtr(void) { return reinterpret_cast<otInstance *>(&InstanceLocator::GetInstance()); }
+    otInstance *GetInstancePtr(void) const { return reinterpret_cast<otInstance *>(&InstanceLocator::GetInstance()); }
 
     Callbacks mCallbacks;
 };
@@ -670,25 +681,16 @@
 //---------------------------------------------------------------------------------------------------------------------
 // Radio APIs that are always mapped to the same `otPlatRadio` function (independent of the link type)
 
-inline const char *Radio::GetVersionString(void)
-{
-    return otPlatRadioGetVersionString(GetInstancePtr());
-}
+inline const char *Radio::GetVersionString(void) { return otPlatRadioGetVersionString(GetInstancePtr()); }
 
 inline void Radio::GetIeeeEui64(Mac::ExtAddress &aIeeeEui64)
 {
     otPlatRadioGetIeeeEui64(GetInstancePtr(), aIeeeEui64.m8);
 }
 
-inline uint32_t Radio::GetSupportedChannelMask(void)
-{
-    return otPlatRadioGetSupportedChannelMask(GetInstancePtr());
-}
+inline uint32_t Radio::GetSupportedChannelMask(void) { return otPlatRadioGetSupportedChannelMask(GetInstancePtr()); }
 
-inline uint32_t Radio::GetPreferredChannelMask(void)
-{
-    return otPlatRadioGetPreferredChannelMask(GetInstancePtr());
-}
+inline uint32_t Radio::GetPreferredChannelMask(void) { return otPlatRadioGetPreferredChannelMask(GetInstancePtr()); }
 
 //---------------------------------------------------------------------------------------------------------------------
 // If IEEE 802.15.4 is among supported radio links, provide inline
@@ -696,20 +698,11 @@
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
 
-inline otRadioCaps Radio::GetCaps(void)
-{
-    return otPlatRadioGetCaps(GetInstancePtr());
-}
+inline otRadioCaps Radio::GetCaps(void) { return otPlatRadioGetCaps(GetInstancePtr()); }
 
-inline int8_t Radio::GetReceiveSensitivity(void)
-{
-    return otPlatRadioGetReceiveSensitivity(GetInstancePtr());
-}
+inline int8_t Radio::GetReceiveSensitivity(void) const { return otPlatRadioGetReceiveSensitivity(GetInstancePtr()); }
 
-inline void Radio::SetPanId(Mac::PanId aPanId)
-{
-    otPlatRadioSetPanId(GetInstancePtr(), aPanId);
-}
+inline void Radio::SetPanId(Mac::PanId aPanId) { otPlatRadioSetPanId(GetInstancePtr(), aPanId); }
 
 inline void Radio::SetMacKey(uint8_t                 aKeyIdMode,
                              uint8_t                 aKeyId,
@@ -728,15 +721,9 @@
     otPlatRadioSetMacKey(GetInstancePtr(), aKeyIdMode, aKeyId, &aPrevKey, &aCurrKey, &aNextKey, aKeyType);
 }
 
-inline Error Radio::GetTransmitPower(int8_t &aPower)
-{
-    return otPlatRadioGetTransmitPower(GetInstancePtr(), &aPower);
-}
+inline Error Radio::GetTransmitPower(int8_t &aPower) { return otPlatRadioGetTransmitPower(GetInstancePtr(), &aPower); }
 
-inline Error Radio::SetTransmitPower(int8_t aPower)
-{
-    return otPlatRadioSetTransmitPower(GetInstancePtr(), aPower);
-}
+inline Error Radio::SetTransmitPower(int8_t aPower) { return otPlatRadioSetTransmitPower(GetInstancePtr(), aPower); }
 
 inline Error Radio::GetCcaEnergyDetectThreshold(int8_t &aThreshold)
 {
@@ -748,45 +735,21 @@
     return otPlatRadioSetCcaEnergyDetectThreshold(GetInstancePtr(), aThreshold);
 }
 
-inline bool Radio::GetPromiscuous(void)
-{
-    return otPlatRadioGetPromiscuous(GetInstancePtr());
-}
+inline bool Radio::GetPromiscuous(void) { return otPlatRadioGetPromiscuous(GetInstancePtr()); }
 
-inline void Radio::SetPromiscuous(bool aEnable)
-{
-    otPlatRadioSetPromiscuous(GetInstancePtr(), aEnable);
-}
+inline void Radio::SetPromiscuous(bool aEnable) { otPlatRadioSetPromiscuous(GetInstancePtr(), aEnable); }
 
-inline otRadioState Radio::GetState(void)
-{
-    return otPlatRadioGetState(GetInstancePtr());
-}
+inline otRadioState Radio::GetState(void) { return otPlatRadioGetState(GetInstancePtr()); }
 
-inline Error Radio::Enable(void)
-{
-    return otPlatRadioEnable(GetInstancePtr());
-}
+inline Error Radio::Enable(void) { return otPlatRadioEnable(GetInstancePtr()); }
 
-inline Error Radio::Disable(void)
-{
-    return otPlatRadioDisable(GetInstancePtr());
-}
+inline Error Radio::Disable(void) { return otPlatRadioDisable(GetInstancePtr()); }
 
-inline bool Radio::IsEnabled(void)
-{
-    return otPlatRadioIsEnabled(GetInstancePtr());
-}
+inline bool Radio::IsEnabled(void) { return otPlatRadioIsEnabled(GetInstancePtr()); }
 
-inline Error Radio::Sleep(void)
-{
-    return otPlatRadioSleep(GetInstancePtr());
-}
+inline Error Radio::Sleep(void) { return otPlatRadioSleep(GetInstancePtr()); }
 
-inline Error Radio::Receive(uint8_t aChannel)
-{
-    return otPlatRadioReceive(GetInstancePtr(), aChannel);
-}
+inline Error Radio::Receive(uint8_t aChannel) { return otPlatRadioReceive(GetInstancePtr(), aChannel); }
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 inline void Radio::UpdateCslSampleTime(uint32_t aCslSampleTime)
@@ -806,17 +769,11 @@
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
-inline uint8_t Radio::GetCslAccuracy(void)
-{
-    return otPlatRadioGetCslAccuracy(GetInstancePtr());
-}
+inline uint8_t Radio::GetCslAccuracy(void) { return otPlatRadioGetCslAccuracy(GetInstancePtr()); }
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
-inline uint8_t Radio::GetCslUncertainty(void)
-{
-    return otPlatRadioGetCslUncertainty(GetInstancePtr());
-}
+inline uint8_t Radio::GetCslUncertainty(void) { return otPlatRadioGetCslUncertainty(GetInstancePtr()); }
 #endif
 
 inline Mac::TxFrame &Radio::GetTransmitBuffer(void)
@@ -824,20 +781,14 @@
     return *static_cast<Mac::TxFrame *>(otPlatRadioGetTransmitBuffer(GetInstancePtr()));
 }
 
-inline int8_t Radio::GetRssi(void)
-{
-    return otPlatRadioGetRssi(GetInstancePtr());
-}
+inline int8_t Radio::GetRssi(void) { return otPlatRadioGetRssi(GetInstancePtr()); }
 
 inline Error Radio::EnergyScan(uint8_t aScanChannel, uint16_t aScanDuration)
 {
     return otPlatRadioEnergyScan(GetInstancePtr(), aScanChannel, aScanDuration);
 }
 
-inline void Radio::EnableSrcMatch(bool aEnable)
-{
-    otPlatRadioEnableSrcMatch(GetInstancePtr(), aEnable);
-}
+inline void Radio::EnableSrcMatch(bool aEnable) { otPlatRadioEnableSrcMatch(GetInstancePtr(), aEnable); }
 
 inline Error Radio::AddSrcMatchShortEntry(Mac::ShortAddress aShortAddress)
 {
@@ -859,15 +810,9 @@
     return otPlatRadioClearSrcMatchExtEntry(GetInstancePtr(), &aExtAddress);
 }
 
-inline void Radio::ClearSrcMatchShortEntries(void)
-{
-    otPlatRadioClearSrcMatchShortEntries(GetInstancePtr());
-}
+inline void Radio::ClearSrcMatchShortEntries(void) { otPlatRadioClearSrcMatchShortEntries(GetInstancePtr()); }
 
-inline void Radio::ClearSrcMatchExtEntries(void)
-{
-    otPlatRadioClearSrcMatchExtEntries(GetInstancePtr());
-}
+inline void Radio::ClearSrcMatchExtEntries(void) { otPlatRadioClearSrcMatchExtEntries(GetInstancePtr()); }
 
 #else //----------------------------------------------------------------------------------------------------------------
 
@@ -876,22 +821,13 @@
     return OT_RADIO_CAPS_ACK_TIMEOUT | OT_RADIO_CAPS_CSMA_BACKOFF | OT_RADIO_CAPS_TRANSMIT_RETRIES;
 }
 
-inline int8_t Radio::GetReceiveSensitivity(void)
-{
-    return -110;
-}
+inline int8_t Radio::GetReceiveSensitivity(void) const { return kDefaultReceiveSensitivity; }
 
-inline void Radio::SetPanId(Mac::PanId)
-{
-}
+inline void Radio::SetPanId(Mac::PanId) {}
 
-inline void Radio::SetExtendedAddress(const Mac::ExtAddress &)
-{
-}
+inline void Radio::SetExtendedAddress(const Mac::ExtAddress &) {}
 
-inline void Radio::SetShortAddress(Mac::ShortAddress)
-{
-}
+inline void Radio::SetShortAddress(Mac::ShortAddress) {}
 
 inline void Radio::SetMacKey(uint8_t,
                              uint8_t,
@@ -901,74 +837,34 @@
 {
 }
 
-inline Error Radio::GetTransmitPower(int8_t &)
-{
-    return kErrorNotImplemented;
-}
+inline Error Radio::GetTransmitPower(int8_t &) { return kErrorNotImplemented; }
 
-inline Error Radio::SetTransmitPower(int8_t)
-{
-    return kErrorNotImplemented;
-}
+inline Error Radio::SetTransmitPower(int8_t) { return kErrorNotImplemented; }
 
-inline Error Radio::GetCcaEnergyDetectThreshold(int8_t &)
-{
-    return kErrorNotImplemented;
-}
+inline Error Radio::GetCcaEnergyDetectThreshold(int8_t &) { return kErrorNotImplemented; }
 
-inline Error Radio::SetCcaEnergyDetectThreshold(int8_t)
-{
-    return kErrorNotImplemented;
-}
+inline Error Radio::SetCcaEnergyDetectThreshold(int8_t) { return kErrorNotImplemented; }
 
-inline bool Radio::GetPromiscuous(void)
-{
-    return false;
-}
+inline bool Radio::GetPromiscuous(void) { return false; }
 
-inline void Radio::SetPromiscuous(bool)
-{
-}
+inline void Radio::SetPromiscuous(bool) {}
 
-inline otRadioState Radio::GetState(void)
-{
-    return OT_RADIO_STATE_DISABLED;
-}
+inline otRadioState Radio::GetState(void) { return OT_RADIO_STATE_DISABLED; }
 
-inline Error Radio::Enable(void)
-{
-    return kErrorNone;
-}
+inline Error Radio::Enable(void) { return kErrorNone; }
 
-inline Error Radio::Disable(void)
-{
-    return kErrorInvalidState;
-}
+inline Error Radio::Disable(void) { return kErrorInvalidState; }
 
-inline bool Radio::IsEnabled(void)
-{
-    return true;
-}
+inline bool Radio::IsEnabled(void) { return true; }
 
-inline Error Radio::Sleep(void)
-{
-    return kErrorNone;
-}
+inline Error Radio::Sleep(void) { return kErrorNone; }
 
-inline Error Radio::Receive(uint8_t)
-{
-    return kErrorNone;
-}
+inline Error Radio::Receive(uint8_t) { return kErrorNone; }
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-inline void Radio::UpdateCslSampleTime(uint32_t)
-{
-}
+inline void Radio::UpdateCslSampleTime(uint32_t) {}
 
-inline Error Radio::ReceiveAt(uint8_t, uint32_t, uint32_t)
-{
-    return kErrorNone;
-}
+inline Error Radio::ReceiveAt(uint8_t, uint32_t, uint32_t) { return kErrorNone; }
 
 inline Error Radio::EnableCsl(uint32_t, otShortAddress aShortAddr, const otExtAddress *)
 {
@@ -977,15 +873,9 @@
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
-inline uint8_t Radio::GetCslAccuracy(void)
-{
-    return UINT8_MAX;
-}
+inline uint8_t Radio::GetCslAccuracy(void) { return UINT8_MAX; }
 
-inline uint8_t Radio::GetCslUncertainty(void)
-{
-    return UINT8_MAX;
-}
+inline uint8_t Radio::GetCslUncertainty(void) { return UINT8_MAX; }
 #endif
 
 inline Mac::TxFrame &Radio::GetTransmitBuffer(void)
@@ -993,52 +883,25 @@
     return *static_cast<Mac::TxFrame *>(otPlatRadioGetTransmitBuffer(GetInstancePtr()));
 }
 
-inline Error Radio::Transmit(Mac::TxFrame &)
-{
-    return kErrorAbort;
-}
+inline Error Radio::Transmit(Mac::TxFrame &) { return kErrorAbort; }
 
-inline int8_t Radio::GetRssi(void)
-{
-    return OT_RADIO_RSSI_INVALID;
-}
+inline int8_t Radio::GetRssi(void) { return kInvalidRssi; }
 
-inline Error Radio::EnergyScan(uint8_t, uint16_t)
-{
-    return kErrorNotImplemented;
-}
+inline Error Radio::EnergyScan(uint8_t, uint16_t) { return kErrorNotImplemented; }
 
-inline void Radio::EnableSrcMatch(bool)
-{
-}
+inline void Radio::EnableSrcMatch(bool) {}
 
-inline Error Radio::AddSrcMatchShortEntry(Mac::ShortAddress)
-{
-    return kErrorNone;
-}
+inline Error Radio::AddSrcMatchShortEntry(Mac::ShortAddress) { return kErrorNone; }
 
-inline Error Radio::AddSrcMatchExtEntry(const Mac::ExtAddress &)
-{
-    return kErrorNone;
-}
+inline Error Radio::AddSrcMatchExtEntry(const Mac::ExtAddress &) { return kErrorNone; }
 
-inline Error Radio::ClearSrcMatchShortEntry(Mac::ShortAddress)
-{
-    return kErrorNone;
-}
+inline Error Radio::ClearSrcMatchShortEntry(Mac::ShortAddress) { return kErrorNone; }
 
-inline Error Radio::ClearSrcMatchExtEntry(const Mac::ExtAddress &)
-{
-    return kErrorNone;
-}
+inline Error Radio::ClearSrcMatchExtEntry(const Mac::ExtAddress &) { return kErrorNone; }
 
-inline void Radio::ClearSrcMatchShortEntries(void)
-{
-}
+inline void Radio::ClearSrcMatchShortEntries(void) {}
 
-inline void Radio::ClearSrcMatchExtEntries(void)
-{
-}
+inline void Radio::ClearSrcMatchExtEntries(void) {}
 
 #endif // #if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
 
diff --git a/src/core/radio/radio_callbacks.cpp b/src/core/radio/radio_callbacks.cpp
index 041d3f4..d23dddc 100644
--- a/src/core/radio/radio_callbacks.cpp
+++ b/src/core/radio/radio_callbacks.cpp
@@ -43,20 +43,14 @@
     Get<Mac::SubMac>().HandleReceiveDone(aFrame, aError);
 }
 
-void Radio::Callbacks::HandleTransmitStarted(Mac::TxFrame &aFrame)
-{
-    Get<Mac::SubMac>().HandleTransmitStarted(aFrame);
-}
+void Radio::Callbacks::HandleTransmitStarted(Mac::TxFrame &aFrame) { Get<Mac::SubMac>().HandleTransmitStarted(aFrame); }
 
 void Radio::Callbacks::HandleTransmitDone(Mac::TxFrame &aFrame, Mac::RxFrame *aAckFrame, Error aError)
 {
     Get<Mac::SubMac>().HandleTransmitDone(aFrame, aAckFrame, aError);
 }
 
-void Radio::Callbacks::HandleEnergyScanDone(int8_t aMaxRssi)
-{
-    Get<Mac::SubMac>().HandleEnergyScanDone(aMaxRssi);
-}
+void Radio::Callbacks::HandleEnergyScanDone(int8_t aMaxRssi) { Get<Mac::SubMac>().HandleEnergyScanDone(aMaxRssi); }
 
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
 void Radio::Callbacks::HandleDiagsReceiveDone(Mac::RxFrame *aFrame, Error aError)
diff --git a/src/core/radio/radio_platform.cpp b/src/core/radio/radio_platform.cpp
index fe994c1..7840a86 100644
--- a/src/core/radio/radio_platform.cpp
+++ b/src/core/radio/radio_platform.cpp
@@ -47,7 +47,7 @@
 
 extern "C" void otPlatRadioReceiveDone(otInstance *aInstance, otRadioFrame *aFrame, otError aError)
 {
-    Instance &    instance = AsCoreType(aInstance);
+    Instance     &instance = AsCoreType(aInstance);
     Mac::RxFrame *rxFrame  = static_cast<Mac::RxFrame *>(aFrame);
 
     VerifyOrExit(instance.IsInitialized());
@@ -67,7 +67,7 @@
 
 extern "C" void otPlatRadioTxStarted(otInstance *aInstance, otRadioFrame *aFrame)
 {
-    Instance &    instance = AsCoreType(aInstance);
+    Instance     &instance = AsCoreType(aInstance);
     Mac::TxFrame &txFrame  = *static_cast<Mac::TxFrame *>(aFrame);
 
     VerifyOrExit(instance.IsInitialized());
@@ -84,7 +84,7 @@
 
 extern "C" void otPlatRadioTxDone(otInstance *aInstance, otRadioFrame *aFrame, otRadioFrame *aAckFrame, otError aError)
 {
-    Instance &    instance = AsCoreType(aInstance);
+    Instance     &instance = AsCoreType(aInstance);
     Mac::TxFrame &txFrame  = *static_cast<Mac::TxFrame *>(aFrame);
     Mac::RxFrame *ackFrame = static_cast<Mac::RxFrame *>(aAckFrame);
 
@@ -145,30 +145,18 @@
 
 #else // #if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
 
-extern "C" void otPlatRadioReceiveDone(otInstance *, otRadioFrame *, otError)
-{
-}
+extern "C" void otPlatRadioReceiveDone(otInstance *, otRadioFrame *, otError) {}
 
-extern "C" void otPlatRadioTxStarted(otInstance *, otRadioFrame *)
-{
-}
+extern "C" void otPlatRadioTxStarted(otInstance *, otRadioFrame *) {}
 
-extern "C" void otPlatRadioTxDone(otInstance *, otRadioFrame *, otRadioFrame *, otError)
-{
-}
+extern "C" void otPlatRadioTxDone(otInstance *, otRadioFrame *, otRadioFrame *, otError) {}
 
-extern "C" void otPlatRadioEnergyScanDone(otInstance *, int8_t)
-{
-}
+extern "C" void otPlatRadioEnergyScanDone(otInstance *, int8_t) {}
 
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
-extern "C" void otPlatDiagRadioReceiveDone(otInstance *, otRadioFrame *, otError)
-{
-}
+extern "C" void otPlatDiagRadioReceiveDone(otInstance *, otRadioFrame *, otError) {}
 
-extern "C" void otPlatDiagRadioTransmitDone(otInstance *, otRadioFrame *, otError)
-{
-}
+extern "C" void otPlatDiagRadioTransmitDone(otInstance *, otRadioFrame *, otError) {}
 #endif
 
 #endif // // #if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
@@ -201,7 +189,7 @@
     return OT_RADIO_STATE_INVALID;
 }
 
-OT_TOOL_WEAK void otPlatRadioSetMacKey(otInstance *            aInstance,
+OT_TOOL_WEAK void otPlatRadioSetMacKey(otInstance             *aInstance,
                                        uint8_t                 aKeyIdMode,
                                        uint8_t                 aKeyId,
                                        const otMacKeyMaterial *aPrevKey,
@@ -224,11 +212,30 @@
     OT_UNUSED_VARIABLE(aMacFrameCounter);
 }
 
-OT_TOOL_WEAK uint64_t otPlatTimeGet(void)
+OT_TOOL_WEAK void otPlatRadioSetMacFrameCounterIfLarger(otInstance *aInstance, uint32_t aMacFrameCounter)
 {
-    return UINT64_MAX;
+    // Radio platforms that support `OT_RADIO_CAPS_TRANSMIT_SEC` should
+    // provide this radio platform function.
+    //
+    // This function helps address an edge-case where OT stack may not
+    // yet know the latest frame counter values used by radio platform
+    // (e.g., due to enhanced acks processed by radio platform directly
+    // or due to delay between RCP and host) and then setting the value
+    // from OT stack may cause the counter value on radio to move back
+    // (OT stack will set the counter after appending it in
+    // `LinkFrameCounterTlv` on all radios when multi-radio links
+    // feature is enabled).
+    //
+    // The weak implementation here is intended as a solution to ensure
+    // temporary compatibility with radio platforms that may not yet
+    // implement it. If this weak implementation is used, the edge-case
+    // above may still happen.
+
+    otPlatRadioSetMacFrameCounter(aInstance, aMacFrameCounter);
 }
 
+OT_TOOL_WEAK uint64_t otPlatTimeGet(void) { return UINT64_MAX; }
+
 OT_TOOL_WEAK uint64_t otPlatRadioGetNow(otInstance *aInstance)
 {
     OT_UNUSED_VARIABLE(aInstance);
diff --git a/src/core/radio/trel_interface.cpp b/src/core/radio/trel_interface.cpp
index 68825e9..7fa1907 100644
--- a/src/core/radio/trel_interface.cpp
+++ b/src/core/radio/trel_interface.cpp
@@ -59,7 +59,7 @@
     , mInitialized(false)
     , mEnabled(false)
     , mFiltered(false)
-    , mRegisterServiceTask(aInstance, HandleRegisterServiceTask)
+    , mRegisterServiceTask(aInstance)
 {
 }
 
@@ -76,6 +76,18 @@
     }
 }
 
+void Interface::SetEnabled(bool aEnable)
+{
+    if (aEnable)
+    {
+        Enable();
+    }
+    else
+    {
+        Disable();
+    }
+}
+
 void Interface::Enable(void)
 {
     VerifyOrExit(!mEnabled);
@@ -127,11 +139,6 @@
     return;
 }
 
-void Interface::HandleRegisterServiceTask(Tasklet &aTasklet)
-{
-    aTasklet.Get<Interface>().RegisterService();
-}
-
 void Interface::RegisterService(void)
 {
     // TXT data consists of two entries: the length fields, the
@@ -178,7 +185,7 @@
 
 void Interface::HandleDiscoveredPeerInfo(const Peer::Info &aInfo)
 {
-    Peer *                 entry;
+    Peer                  *entry;
     Mac::ExtAddress        extAddress;
     MeshCoP::ExtendedPanId extPanId;
     bool                   isNew = false;
@@ -239,8 +246,8 @@
     return;
 }
 
-Error Interface::ParsePeerInfoTxtData(const Peer::Info &      aInfo,
-                                      Mac::ExtAddress &       aExtAddress,
+Error Interface::ParsePeerInfoTxtData(const Peer::Info       &aInfo,
+                                      Mac::ExtAddress        &aExtAddress,
                                       MeshCoP::ExtendedPanId &aExtPanId) const
 {
     Error                   error;
@@ -255,6 +262,15 @@
 
     while ((error = iterator.GetNextEntry(entry)) == kErrorNone)
     {
+        // If the TXT data happens to have entries with key longer
+        // than `kMaxKeyLength`, `mKey` would be `nullptr` and full
+        // entry would be placed in `mValue`. We skip over such
+        // entries.
+        if (entry.mKey == nullptr)
+        {
+            continue;
+        }
+
         if (strcmp(entry.mKey, kTxtRecordExtAddressKey) == 0)
         {
             VerifyOrExit(!parsedExtAddress, error = kErrorParse);
diff --git a/src/core/radio/trel_interface.hpp b/src/core/radio/trel_interface.hpp
index b80bf1b..1822d47 100644
--- a/src/core/radio/trel_interface.hpp
+++ b/src/core/radio/trel_interface.hpp
@@ -134,7 +134,7 @@
         {
         public:
             bool                 IsRemoved(void) const { return mRemoved; }
-            const uint8_t *      GetTxtData(void) const { return mTxtData; }
+            const uint8_t       *GetTxtData(void) const { return mTxtData; }
             uint16_t             GetTxtLength(void) const { return mTxtLength; }
             const Ip6::SockAddr &GetSockAddr(void) const { return static_cast<const Ip6::SockAddr &>(mSockAddr); }
         };
@@ -152,6 +152,13 @@
     typedef otTrelPeerIterator PeerIterator;
 
     /**
+     * This method enables or disables the TREL interface.
+     *
+     * @param[in] aEnable A boolean to enable/disable the TREL interface.
+     */
+    void SetEnabled(bool aEnable);
+
+    /**
      * This method enables the TREL interface.
      *
      * This call initiates an ongoing DNS-SD browse on the service name "_trel._udp" within the local browsing domain
@@ -242,21 +249,22 @@
     void HandleReceived(uint8_t *aBuffer, uint16_t aLength);
     void HandleDiscoveredPeerInfo(const Peer::Info &aInfo);
 
-    static void HandleRegisterServiceTask(Tasklet &aTasklet);
-    void        RegisterService(void);
-    Error       ParsePeerInfoTxtData(const Peer::Info &      aInfo,
-                                     Mac::ExtAddress &       aExtAddress,
-                                     MeshCoP::ExtendedPanId &aExtPanId) const;
-    Peer *      GetNewPeerEntry(void);
-    void        RemovePeerEntry(Peer &aEntry);
+    void  RegisterService(void);
+    Error ParsePeerInfoTxtData(const Peer::Info       &aInfo,
+                               Mac::ExtAddress        &aExtAddress,
+                               MeshCoP::ExtendedPanId &aExtPanId) const;
+    Peer *GetNewPeerEntry(void);
+    void  RemovePeerEntry(Peer &aEntry);
 
-    bool      mInitialized : 1;
-    bool      mEnabled : 1;
-    bool      mFiltered : 1;
-    Tasklet   mRegisterServiceTask;
-    uint16_t  mUdpPort;
-    Packet    mRxPacket;
-    PeerTable mPeerTable;
+    using RegisterServiceTask = TaskletIn<Interface, &Interface::RegisterService>;
+
+    bool                mInitialized : 1;
+    bool                mEnabled : 1;
+    bool                mFiltered : 1;
+    RegisterServiceTask mRegisterServiceTask;
+    uint16_t            mUdpPort;
+    Packet              mRxPacket;
+    PeerTable           mPeerTable;
 };
 
 } // namespace Trel
diff --git a/src/core/radio/trel_link.cpp b/src/core/radio/trel_link.cpp
index 549633b..8eb2b9e 100644
--- a/src/core/radio/trel_link.cpp
+++ b/src/core/radio/trel_link.cpp
@@ -51,8 +51,8 @@
     , mRxChannel(0)
     , mPanId(Mac::kPanIdBroadcast)
     , mTxPacketNumber(0)
-    , mTxTasklet(aInstance, HandleTxTasklet)
-    , mTimer(aInstance, HandleTimer)
+    , mTxTasklet(aInstance)
+    , mTimer(aInstance)
     , mInterface(aInstance)
 {
     memset(&mTxFrame, 0, sizeof(mTxFrame));
@@ -70,10 +70,7 @@
     mTimer.Start(kAckWaitWindow);
 }
 
-void Link::AfterInit(void)
-{
-    mInterface.Init();
-}
+void Link::AfterInit(void) { mInterface.Init(); }
 
 void Link::Enable(void)
 {
@@ -116,15 +113,7 @@
     mTxTasklet.Post();
 }
 
-void Link::HandleTxTasklet(Tasklet &aTasklet)
-{
-    aTasklet.Get<Link>().HandleTxTasklet();
-}
-
-void Link::HandleTxTasklet(void)
-{
-    BeginTransmit();
-}
+void Link::HandleTxTasklet(void) { BeginTransmit(); }
 
 void Link::BeginTransmit(void)
 {
@@ -132,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);
 
@@ -181,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);
         }
     }
 
@@ -223,15 +212,15 @@
 
     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())
     {
-        uint16_t fcf = Mac::Frame::kFcfFrameAck;
+        uint16_t fcf = Mac::Frame::kTypeAck;
 
         if (!Get<Mle::MleRouter>().IsRxOnWhenIdle())
         {
-            fcf |= Mac::Frame::kFcfFramePending;
+            fcf |= kFcfFramePending;
         }
 
         // Prepare the ack frame (FCF followed by sequence number)
@@ -245,7 +234,7 @@
         mRxFrame.mRadioType = Mac::kRadioTypeTrel;
 #endif
         mRxFrame.mInfo.mRxInfo.mTimestamp             = 0;
-        mRxFrame.mInfo.mRxInfo.mRssi                  = OT_RADIO_RSSI_INVALID;
+        mRxFrame.mInfo.mRxInfo.mRssi                  = Radio::kInvalidRssi;
         mRxFrame.mInfo.mRxInfo.mLqi                   = OT_RADIO_LQI_NONE;
         mRxFrame.mInfo.mRxInfo.mAckedWithFramePending = false;
 
@@ -266,11 +255,6 @@
     Get<Mac::Mac>().HandleTransmitDone(mTxFrame, aAckFrame, aError);
 }
 
-void Link::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Link>().HandleTimer();
-}
-
 void Link::HandleTimer(void)
 {
     mTimer.Start(kAckWaitWindow);
@@ -281,7 +265,7 @@
         HandleTimer(child);
     }
 
-    for (Router &router : Get<RouterTable>().Iterate())
+    for (Router &router : Get<RouterTable>())
     {
         HandleTimer(router);
     }
@@ -401,7 +385,7 @@
 {
     Error        ackError;
     Mac::Address srcAddress;
-    Neighbor *   neighbor;
+    Neighbor    *neighbor;
     uint32_t     ackNumber;
 
     LogDebg("HandleAck() [%s]", aAckPacket.GetHeader().ToString().AsCString());
diff --git a/src/core/radio/trel_link.hpp b/src/core/radio/trel_link.hpp
index fd00ac5..f19b0bb 100644
--- a/src/core/radio/trel_link.hpp
+++ b/src/core/radio/trel_link.hpp
@@ -153,6 +153,7 @@
     static constexpr uint16_t k154AckFrameSize = 3 + kFcsSize;
     static constexpr int8_t   kRxRssi          = -20; // The RSSI value used for received frames on TREL radio link.
     static constexpr uint32_t kAckWaitWindow   = 750; // (in msec)
+    static constexpr uint16_t kFcfFramePending = 1 << 4;
 
     enum State : uint8_t
     {
@@ -173,21 +174,20 @@
     void ReportDeferredAckStatus(Neighbor &aNeighbor, Error aError);
     void HandleTimer(Neighbor &aNeighbor);
     void HandleNotifierEvents(Events aEvents);
-
-    static void HandleTxTasklet(Tasklet &aTasklet);
-    void        HandleTxTasklet(void);
-
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTxTasklet(void);
+    void HandleTimer(void);
 
     static const char *StateToString(State aState);
 
+    using TxTasklet    = TaskletIn<Link, &Link::HandleTxTasklet>;
+    using TimeoutTimer = TimerMilliIn<Link, &Link::HandleTimer>;
+
     State        mState;
     uint8_t      mRxChannel;
     Mac::PanId   mPanId;
     uint32_t     mTxPacketNumber;
-    Tasklet      mTxTasklet;
-    TimerMilli   mTimer;
+    TxTasklet    mTxTasklet;
+    TimeoutTimer mTimer;
     Interface    mInterface;
     Mac::RxFrame mRxFrame;
     Mac::TxFrame mTxFrame;
diff --git a/src/core/radio/trel_packet.cpp b/src/core/radio/trel_packet.cpp
index 9b5cabd..2847b60 100644
--- a/src/core/radio/trel_packet.cpp
+++ b/src/core/radio/trel_packet.cpp
@@ -94,7 +94,8 @@
         break;
     }
 
-    string.Append(" panid:%04x num:%lu src:%s", GetPanId(), GetPacketNumber(), GetSource().ToString().AsCString());
+    string.Append(" panid:%04x num:%lu src:%s", GetPanId(), ToUlong(GetPacketNumber()),
+                  GetSource().ToString().AsCString());
 
     if ((type == kTypeUnicast) || (type == kTypeAck))
     {
diff --git a/src/core/radio_cli.cmake b/src/core/radio_cli.cmake
index b4e26fe..31e7b6d 100644
--- a/src/core/radio_cli.cmake
+++ b/src/core/radio_cli.cmake
@@ -54,5 +54,6 @@
 target_link_libraries(openthread-radio-cli
     PRIVATE
         ${OT_MBEDTLS_RCP}
+        ot-config-radio
         ot-config
 )
diff --git a/src/core/thread/address_resolver.cpp b/src/core/thread/address_resolver.cpp
index 9876e93..16bf8e7 100644
--- a/src/core/thread/address_resolver.cpp
+++ b/src/core/thread/address_resolver.cpp
@@ -54,19 +54,12 @@
 
 AddressResolver::AddressResolver(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mAddressError(UriPath::kAddressError, &AddressResolver::HandleAddressError, this)
 #if OPENTHREAD_FTD
-    , mAddressQuery(UriPath::kAddressQuery, &AddressResolver::HandleAddressQuery, this)
-    , mAddressNotification(UriPath::kAddressNotify, &AddressResolver::HandleAddressNotification, this)
     , mCacheEntryPool(aInstance)
     , mIcmpHandler(&AddressResolver::HandleIcmpReceive, this)
 #endif
 {
-    Get<Tmf::Agent>().AddResource(mAddressError);
 #if OPENTHREAD_FTD
-    Get<Tmf::Agent>().AddResource(mAddressQuery);
-    Get<Tmf::Agent>().AddResource(mAddressNotification);
-
     IgnoreError(Get<Ip6::Icmp>().RegisterHandler(mIcmpHandler));
 #endif
 }
@@ -96,11 +89,8 @@
 Error AddressResolver::GetNextCacheEntry(EntryInfo &aInfo, Iterator &aIterator) const
 {
     Error                 error = kErrorNone;
-    const CacheEntryList *list;
-    const CacheEntry *    entry;
-
-    list  = reinterpret_cast<const CacheEntryList *>(aIterator.mData[kIteratorListIndex]);
-    entry = reinterpret_cast<const CacheEntry *>(aIterator.mData[kIteratorEntryIndex]);
+    const CacheEntryList *list  = aIterator.GetList();
+    const CacheEntry     *entry = aIterator.GetEntry();
 
     while (entry == nullptr)
     {
@@ -130,16 +120,16 @@
 
     // Update the iterator then populate the `aInfo`.
 
-    aIterator.mData[kIteratorEntryIndex] = entry->GetNext();
-    aIterator.mData[kIteratorListIndex]  = list;
+    aIterator.SetEntry(entry->GetNext());
+    aIterator.SetList(list);
 
-    memset(&aInfo, 0, sizeof(aInfo));
+    aInfo.Clear();
     aInfo.mTarget = entry->GetTarget();
     aInfo.mRloc16 = entry->GetRloc16();
 
     if (list == &mCachedList)
     {
-        aInfo.mState          = OT_CACHE_ENTRY_STATE_CACHED;
+        aInfo.mState          = MapEnum(EntryInfo::kStateCached);
         aInfo.mCanEvict       = true;
         aInfo.mValidLastTrans = entry->IsLastTransactionTimeValid();
 
@@ -154,15 +144,15 @@
 
     if (list == &mSnoopedList)
     {
-        aInfo.mState = OT_CACHE_ENTRY_STATE_SNOOPED;
+        aInfo.mState = MapEnum(EntryInfo::kStateSnooped);
     }
     else if (list == &mQueryList)
     {
-        aInfo.mState = OT_CACHE_ENTRY_STATE_QUERY;
+        aInfo.mState = MapEnum(EntryInfo::kStateQuery);
     }
     else
     {
-        aInfo.mState = OT_CACHE_ENTRY_STATE_RETRY_QUERY;
+        aInfo.mState = MapEnum(EntryInfo::kStateRetryQuery);
     }
 
     aInfo.mCanEvict   = entry->CanEvict();
@@ -173,15 +163,12 @@
     return error;
 }
 
-void AddressResolver::Remove(uint8_t aRouterId)
+void AddressResolver::RemoveEntriesForRouterId(uint8_t aRouterId)
 {
-    Remove(Mle::Mle::Rloc16FromRouterId(aRouterId), /* aMatchRouterId */ true);
+    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)
 {
@@ -199,7 +186,7 @@
 
         while ((entry = GetEntryAfter(prev, *list)) != nullptr)
         {
-            if ((aMatchRouterId && Mle::Mle::RouterIdMatch(entry->GetRloc16(), aRloc16)) ||
+            if ((aMatchRouterId && Mle::RouterIdMatch(entry->GetRloc16(), aRloc16)) ||
                 (!aMatchRouterId && (entry->GetRloc16() == aRloc16)))
             {
                 RemoveCacheEntry(*entry, *list, prev, aMatchRouterId ? kReasonRemovingRouterId : kReasonRemovingRloc16);
@@ -217,10 +204,10 @@
 }
 
 AddressResolver::CacheEntry *AddressResolver::FindCacheEntry(const Ip6::Address &aEid,
-                                                             CacheEntryList *&   aList,
-                                                             CacheEntry *&       aPrevEntry)
+                                                             CacheEntryList    *&aList,
+                                                             CacheEntry        *&aPrevEntry)
 {
-    CacheEntry *    entry   = nullptr;
+    CacheEntry     *entry   = nullptr;
     CacheEntryList *lists[] = {&mCachedList, &mSnoopedList, &mQueryList, &mQueryRetryList};
 
     for (CacheEntryList *list : lists)
@@ -234,15 +221,12 @@
     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)
 {
-    CacheEntry *    entry;
-    CacheEntry *    prev;
+    CacheEntry     *entry;
+    CacheEntry     *prev;
     CacheEntryList *list;
 
     entry = FindCacheEntry(aEid, list, prev);
@@ -255,10 +239,26 @@
     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;
-    CacheEntry *    prevEntry = nullptr;
+    CacheEntry     *newEntry  = nullptr;
+    CacheEntry     *prevEntry = nullptr;
     CacheEntryList *lists[]   = {&mSnoopedList, &mQueryRetryList, &mQueryList, &mCachedList};
 
     // The following order is used when trying to allocate a new cache
@@ -321,9 +321,9 @@
     return newEntry;
 }
 
-void AddressResolver::RemoveCacheEntry(CacheEntry &    aEntry,
+void AddressResolver::RemoveCacheEntry(CacheEntry     &aEntry,
                                        CacheEntryList &aList,
-                                       CacheEntry *    aPrevEntry,
+                                       CacheEntry     *aPrevEntry,
                                        Reason          aReason)
 {
     aList.PopAfter(aPrevEntry);
@@ -344,8 +344,8 @@
 
     Error           error = kErrorNone;
     CacheEntryList *list;
-    CacheEntry *    entry;
-    CacheEntry *    prev;
+    CacheEntry     *entry;
+    CacheEntry     *prev;
 
     entry = FindCacheEntry(aEid, list, prev);
     VerifyOrExit(entry != nullptr, error = kErrorNotFound);
@@ -381,11 +381,15 @@
                                               Mac::ShortAddress   aDest)
 {
     uint16_t          numNonEvictable = 0;
-    CacheEntry *      entry;
+    CacheEntry       *entry;
     Mac::ShortAddress macAddress;
 
     VerifyOrExit(Get<Mle::MleRouter>().IsFullThreadDevice());
 
+#if OPENTHREAD_CONFIG_TMF_ALLOW_ADDRESS_RESOLUTION_USING_NET_DATA_SERVICES
+    VerifyOrExit(ResolveUsingNetDataServices(aEid, macAddress) != kErrorNone);
+#endif
+
     VerifyOrExit(UpdateCacheEntry(aEid, aRloc16) != kErrorNone);
 
     // Skip if the `aRloc16` (i.e., the source of the snooped message)
@@ -476,10 +480,14 @@
 Error AddressResolver::Resolve(const Ip6::Address &aEid, Mac::ShortAddress &aRloc16, bool aAllowAddressQuery)
 {
     Error           error = kErrorNone;
-    CacheEntry *    entry;
-    CacheEntry *    prev = nullptr;
+    CacheEntry     *entry;
+    CacheEntry     *prev = nullptr;
     CacheEntryList *list;
 
+#if OPENTHREAD_CONFIG_TMF_ALLOW_ADDRESS_RESOLUTION_USING_NET_DATA_SERVICES
+    VerifyOrExit(ResolveUsingNetDataServices(aEid, aRloc16) != kErrorNone);
+#endif
+
     entry = FindCacheEntry(aEid, list, prev);
 
     if (entry == nullptr)
@@ -558,13 +566,48 @@
     return error;
 }
 
+#if OPENTHREAD_CONFIG_TMF_ALLOW_ADDRESS_RESOLUTION_USING_NET_DATA_SERVICES
+
+Error AddressResolver::ResolveUsingNetDataServices(const Ip6::Address &aEid, Mac::ShortAddress &aRloc16)
+{
+    // Tries to resolve `aEid` Network Data DNS/SRP Unicast address
+    // service entries.  Returns `kErrorNone` and updates `aRloc16`
+    // if successful, otherwise returns `kErrorNotFound`.
+
+    Error                                     error = kErrorNotFound;
+    NetworkData::Service::Manager::Iterator   iterator;
+    NetworkData::Service::DnsSrpUnicast::Info unicastInfo;
+
+    VerifyOrExit(Get<Mle::Mle>().GetDeviceMode().GetNetworkDataType() == NetworkData::kFullSet);
+
+    while (Get<NetworkData::Service::Manager>().GetNextDnsSrpUnicastInfo(iterator, unicastInfo) == kErrorNone)
+    {
+        if (unicastInfo.mOrigin != NetworkData::Service::DnsSrpUnicast::kFromServerData)
+        {
+            continue;
+        }
+
+        if (aEid == unicastInfo.mSockAddr.GetAddress())
+        {
+            aRloc16 = unicastInfo.mRloc16;
+            error   = kErrorNone;
+            ExitNow();
+        }
+    }
+
+exit:
+    return error;
+}
+
+#endif // OPENTHREAD_CONFIG_TMF_ALLOW_ADDRESS_RESOLUTION_USING_NET_DATA_SERVICES
+
 Error AddressResolver::SendAddressQuery(const Ip6::Address &aEid)
 {
     Error            error;
-    Coap::Message *  message;
+    Coap::Message   *message;
     Tmf::MessageInfo messageInfo(GetInstance());
 
-    message = Get<Tmf::Agent>().NewPriorityNonConfirmablePostMessage(UriPath::kAddressQuery);
+    message = Get<Tmf::Agent>().NewPriorityNonConfirmablePostMessage(kUriAddressQuery);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = Tlv::Append<ThreadTargetTlv>(*message, aEid));
@@ -573,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:
 
@@ -585,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
@@ -594,21 +637,16 @@
     return error;
 }
 
-void AddressResolver::HandleAddressNotification(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<AddressResolver *>(aContext)->HandleAddressNotification(AsCoapMessage(aMessage),
-                                                                        AsCoreType(aMessageInfo));
-}
-
-void AddressResolver::HandleAddressNotification(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <>
+void AddressResolver::HandleTmf<kUriAddressNotify>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Ip6::Address             target;
     Ip6::InterfaceIdentifier meshLocalIid;
     uint16_t                 rloc16;
     uint32_t                 lastTransactionTime;
-    CacheEntryList *         list;
-    CacheEntry *             entry;
-    CacheEntry *             prev;
+    CacheEntryList          *list;
+    CacheEntry              *entry;
+    CacheEntry              *prev;
 
     VerifyOrExit(aMessage.IsConfirmablePostRequest());
 
@@ -627,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);
@@ -659,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);
@@ -668,18 +706,18 @@
     return;
 }
 
-void AddressResolver::SendAddressError(const Ip6::Address &            aTarget,
+void AddressResolver::SendAddressError(const Ip6::Address             &aTarget,
                                        const Ip6::InterfaceIdentifier &aMeshLocalIid,
-                                       const Ip6::Address *            aDestination)
+                                       const Ip6::Address             *aDestination)
 {
     Error            error;
-    Coap::Message *  message;
+    Coap::Message   *message;
     Tmf::MessageInfo messageInfo(GetInstance());
 
     VerifyOrExit((message = Get<Tmf::Agent>().NewMessage()) != nullptr, error = kErrorNoBufs);
 
     message->Init(aDestination == nullptr ? Coap::kTypeNonConfirmable : Coap::kTypeConfirmable, Coap::kCodePost);
-    SuccessOrExit(error = message->AppendUriPathOptions(UriPath::kAddressError));
+    SuccessOrExit(error = message->AppendUriPathOptions(PathForUri(kUriAddressError)));
     SuccessOrExit(error = message->SetPayloadMarker());
 
     SuccessOrExit(error = Tlv::Append<ThreadTargetTlv>(*message, aTarget));
@@ -696,25 +734,21 @@
 
     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));
     }
 }
 
 #endif // OPENTHREAD_FTD
 
-void AddressResolver::HandleAddressError(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<AddressResolver *>(aContext)->HandleAddressError(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void AddressResolver::HandleAddressError(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <>
+void AddressResolver::HandleTmf<kUriAddressError>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error                    error = kErrorNone;
     Ip6::Address             target;
@@ -726,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>());
         }
     }
 
@@ -789,18 +823,14 @@
 
     if (error != kErrorNone)
     {
-        LogWarn("Error while processing address error notification: %s", ErrorToString(error));
+        LogWarn("Error %s when processing %s", ErrorToString(error), UriToString<kUriAddressError>());
     }
 }
 
 #if OPENTHREAD_FTD
 
-void AddressResolver::HandleAddressQuery(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<AddressResolver *>(aContext)->HandleAddressQuery(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void AddressResolver::HandleAddressQuery(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <>
+void AddressResolver::HandleTmf<kUriAddressQuery>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Ip6::Address target;
     uint32_t     lastTransactionTime;
@@ -809,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))
     {
@@ -839,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
@@ -848,16 +879,16 @@
     return;
 }
 
-void AddressResolver::SendAddressQueryResponse(const Ip6::Address &            aTarget,
+void AddressResolver::SendAddressQueryResponse(const Ip6::Address             &aTarget,
                                                const Ip6::InterfaceIdentifier &aMeshLocalIid,
-                                               const uint32_t *                aLastTransactionTime,
-                                               const Ip6::Address &            aDestination)
+                                               const uint32_t                 *aLastTransactionTime,
+                                               const Ip6::Address             &aDestination)
 {
     Error            error;
-    Coap::Message *  message;
+    Coap::Message   *message;
     Tmf::MessageInfo messageInfo(GetInstance());
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kAddressNotify);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriAddressNotify);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = Tlv::Append<ThreadTargetTlv>(*message, aTarget));
@@ -873,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);
@@ -941,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);
@@ -962,8 +993,8 @@
     }
 }
 
-void AddressResolver::HandleIcmpReceive(void *               aContext,
-                                        otMessage *          aMessage,
+void AddressResolver::HandleIcmpReceive(void                *aContext,
+                                        otMessage           *aMessage,
                                         const otMessageInfo *aMessageInfo,
                                         const otIcmp6Header *aIcmpHeader)
 {
@@ -973,8 +1004,8 @@
                                                                 AsCoreType(aIcmpHeader));
 }
 
-void AddressResolver::HandleIcmpReceive(Message &                aMessage,
-                                        const Ip6::MessageInfo & aMessageInfo,
+void AddressResolver::HandleIcmpReceive(Message                 &aMessage,
+                                        const Ip6::MessageInfo  &aMessageInfo,
                                         const Ip6::Icmp::Header &aIcmpHeader)
 {
     OT_UNUSED_VARIABLE(aMessageInfo);
@@ -998,7 +1029,7 @@
 void AddressResolver::LogCacheEntryChange(EntryChange       aChange,
                                           Reason            aReason,
                                           const CacheEntry &aEntry,
-                                          CacheEntryList *  aList)
+                                          CacheEntryList   *aList)
 {
     static const char *const kChangeStrings[] = {
         "added",   // (0) kEntryAdded
@@ -1049,9 +1080,7 @@
 
 #else // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
-void AddressResolver::LogCacheEntryChange(EntryChange, Reason, const CacheEntry &, CacheEntryList *)
-{
-}
+void AddressResolver::LogCacheEntryChange(EntryChange, Reason, const CacheEntry &, CacheEntryList *) {}
 
 #endif // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_NOTE)
 
diff --git a/src/core/thread/address_resolver.hpp b/src/core/thread/address_resolver.hpp
index b65fc60..81ee36b 100644
--- a/src/core/thread/address_resolver.hpp
+++ b/src/core/thread/address_resolver.hpp
@@ -37,6 +37,7 @@
 #include "openthread-core-config.h"
 
 #include "coap/coap.hpp"
+#include "common/as_core_type.hpp"
 #include "common/linked_list.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
@@ -46,6 +47,7 @@
 #include "net/icmp6.hpp"
 #include "net/udp6.hpp"
 #include "thread/thread_tlvs.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -65,19 +67,44 @@
 class AddressResolver : public InstanceLocator, private NonCopyable
 {
     friend class TimeTicker;
+    friend class Tmf::Agent;
+
+    class CacheEntry;
+    class CacheEntryList;
 
 public:
     /**
      * This type represents an iterator used for iterating through the EID cache table entries.
      *
      */
-    typedef otCacheEntryIterator Iterator;
+    class Iterator : public otCacheEntryIterator, public Clearable<Iterator>
+    {
+        friend class AddressResolver;
+
+        static constexpr uint8_t kListIndex  = 0;
+        static constexpr uint8_t kEntryIndex = 1;
+
+        const CacheEntry     *GetEntry(void) const { return static_cast<const CacheEntry *>(mData[kEntryIndex]); }
+        void                  SetEntry(const CacheEntry *aEntry) { mData[kEntryIndex] = aEntry; }
+        const CacheEntryList *GetList(void) const { return static_cast<const CacheEntryList *>(mData[kListIndex]); }
+        void                  SetList(const CacheEntryList *aList) { mData[kListIndex] = aList; }
+    };
 
     /**
      * This type represents an EID cache entry.
      *
      */
-    typedef otCacheEntryInfo EntryInfo;
+    class EntryInfo : public otCacheEntryInfo, public Clearable<EntryInfo>
+    {
+    public:
+        enum State : uint8_t ///< Entry state.
+        {
+            kStateCached     = OT_CACHE_ENTRY_STATE_CACHED,      ///< Cached and in-use.
+            kStateSnooped    = OT_CACHE_ENTRY_STATE_SNOOPED,     ///< Created by snoop optimization.
+            kStateQuery      = OT_CACHE_ENTRY_STATE_QUERY,       ///< Ongoing query for the EID.
+            kStateRetryQuery = OT_CACHE_ENTRY_STATE_RETRY_QUERY, ///< In retry wait mode.
+        };
+    };
 
     /**
      * This constructor initializes the object.
@@ -112,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.
@@ -120,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.
@@ -128,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.
@@ -188,10 +224,10 @@
      * @param[in]  aDestination             The destination to send the ADDR_NTF.ans message.
      *
      */
-    void SendAddressQueryResponse(const Ip6::Address &            aTarget,
+    void SendAddressQueryResponse(const Ip6::Address             &aTarget,
                                   const Ip6::InterfaceIdentifier &aMeshLocalIid,
-                                  const uint32_t *                aLastTransactionTimeTlv,
-                                  const Ip6::Address &            aDestination);
+                                  const uint32_t                 *aLastTransactionTimeTlv,
+                                  const Ip6::Address             &aDestination);
 
     /**
      * This method sends an Address Error Notification (ADDR_ERR.ntf) message.
@@ -201,9 +237,9 @@
      * @param aDestination   The destination to send the ADDR_ERR.ntf message.
      *
      */
-    void SendAddressError(const Ip6::Address &            aTarget,
+    void SendAddressError(const Ip6::Address             &aTarget,
                           const Ip6::InterfaceIdentifier &aMeshLocalIid,
-                          const Ip6::Address *            aDestination);
+                          const Ip6::Address             *aDestination);
 
 private:
     static constexpr uint16_t kCacheEntries                  = OPENTHREAD_CONFIG_TMF_ADDRESS_CACHE_ENTRIES;
@@ -215,15 +251,12 @@
     static constexpr uint16_t kAddressQueryMaxRetryDelay     = OPENTHREAD_CONFIG_TMF_ADDRESS_QUERY_MAX_RETRY_DELAY;
     static constexpr uint16_t kSnoopBlockEvictionTimeout     = OPENTHREAD_CONFIG_TMF_SNOOP_CACHE_ENTRY_TIMEOUT;
 
-    static constexpr uint8_t kIteratorListIndex  = 0;
-    static constexpr uint8_t kIteratorEntryIndex = 1;
-
     class CacheEntry : public InstanceLocatorInit
     {
     public:
         void Init(Instance &aInstance);
 
-        CacheEntry *      GetNext(void);
+        CacheEntry       *GetNext(void);
         const CacheEntry *GetNext(void) const;
         void              SetNext(CacheEntry *aEntry);
 
@@ -281,7 +314,10 @@
     };
 
     typedef Pool<CacheEntry, kCacheEntries> CacheEntryPool;
-    typedef LinkedList<CacheEntry>          CacheEntryList;
+
+    class CacheEntryList : public LinkedList<CacheEntry>
+    {
+    };
 
     enum EntryChange : uint8_t
     {
@@ -311,62 +347,60 @@
     CacheEntry *NewCacheEntry(bool aSnoopedEntry);
     void        RemoveCacheEntry(CacheEntry &aEntry, CacheEntryList &aList, CacheEntry *aPrevEntry, Reason aReason);
     Error       UpdateCacheEntry(const Ip6::Address &aEid, Mac::ShortAddress aRloc16);
-
-    Error SendAddressQuery(const Ip6::Address &aEid);
+    Error       SendAddressQuery(const Ip6::Address &aEid);
+#if OPENTHREAD_CONFIG_TMF_ALLOW_ADDRESS_RESOLUTION_USING_NET_DATA_SERVICES
+    Error ResolveUsingNetDataServices(const Ip6::Address &aEid, Mac::ShortAddress &aRloc16);
+#endif
 
     static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
 
 #endif // OPENTHREAD_FTD
 
-    static void HandleAddressError(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleAddressError(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
 #if OPENTHREAD_FTD
-    static void HandleAddressQuery(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleAddressQuery(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static void HandleAddressNotification(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleAddressNotification(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-
-    static void HandleIcmpReceive(void *               aContext,
-                                  otMessage *          aMessage,
+    static void HandleIcmpReceive(void                *aContext,
+                                  otMessage           *aMessage,
                                   const otMessageInfo *aMessageInfo,
                                   const otIcmp6Header *aIcmpHeader);
-    void        HandleIcmpReceive(Message &                aMessage,
-                                  const Ip6::MessageInfo & aMessageInfo,
+    void        HandleIcmpReceive(Message                 &aMessage,
+                                  const Ip6::MessageInfo  &aMessageInfo,
                                   const Ip6::Icmp::Header &aIcmpHeader);
 
-    void HandleTimeTick(void);
-
-    void LogCacheEntryChange(EntryChange       aChange,
-                             Reason            aReason,
-                             const CacheEntry &aEntry,
-                             CacheEntryList *  aList = nullptr);
-
+    void        HandleTimeTick(void);
+    void        LogCacheEntryChange(EntryChange       aChange,
+                                    Reason            aReason,
+                                    const CacheEntry &aEntry,
+                                    CacheEntryList   *aList = nullptr);
     const char *ListToString(const CacheEntryList *aList) const;
 
     static AddressResolver::CacheEntry *GetEntryAfter(CacheEntry *aPrev, CacheEntryList &aList);
 
-#endif // OPENTHREAD_FTD
-    Coap::Resource mAddressError;
-#if OPENTHREAD_FTD
-    Coap::Resource mAddressQuery;
-    Coap::Resource mAddressNotification;
-
-    CacheEntryPool mCacheEntryPool;
-    CacheEntryList mCachedList;
-    CacheEntryList mSnoopedList;
-    CacheEntryList mQueryList;
-    CacheEntryList mQueryRetryList;
-
+    CacheEntryPool     mCacheEntryPool;
+    CacheEntryList     mCachedList;
+    CacheEntryList     mSnoopedList;
+    CacheEntryList     mQueryList;
+    CacheEntryList     mQueryRetryList;
     Ip6::Icmp::Handler mIcmpHandler;
-#endif //  OPENTHREAD_FTD
+
+#endif // OPENTHREAD_FTD
 };
 
+DeclareTmfHandler(AddressResolver, kUriAddressError);
+#if OPENTHREAD_FTD
+DeclareTmfHandler(AddressResolver, kUriAddressQuery);
+DeclareTmfHandler(AddressResolver, kUriAddressNotify);
+#endif
+
 /**
  * @}
  */
 
+DefineCoreType(otCacheEntryIterator, AddressResolver::Iterator);
+DefineCoreType(otCacheEntryInfo, AddressResolver::EntryInfo);
+DefineMapEnum(otCacheEntryState, AddressResolver::EntryInfo::State);
+
 } // namespace ot
 
 #endif // ADDRESS_RESOLVER_HPP_
diff --git a/src/core/thread/announce_begin_server.cpp b/src/core/thread/announce_begin_server.cpp
index ddac782..1c904e3 100644
--- a/src/core/thread/announce_begin_server.cpp
+++ b/src/core/thread/announce_begin_server.cpp
@@ -52,9 +52,7 @@
 
 AnnounceBeginServer::AnnounceBeginServer(Instance &aInstance)
     : AnnounceSenderBase(aInstance, AnnounceBeginServer::HandleTimer)
-    , mAnnounceBegin(UriPath::kAnnounceBegin, &AnnounceBeginServer::HandleRequest, this)
 {
-    Get<Tmf::Agent>().AddResource(mAnnounceBegin);
 }
 
 void AnnounceBeginServer::SendAnnounce(uint32_t aChannelMask, uint8_t aCount, uint16_t aPeriod)
@@ -65,17 +63,12 @@
     AnnounceSenderBase::SendAnnounce(aCount);
 }
 
-void AnnounceBeginServer::HandleRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
+template <>
+void AnnounceBeginServer::HandleTmf<kUriAnnounceBegin>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    static_cast<AnnounceBeginServer *>(aContext)->HandleRequest(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void AnnounceBeginServer::HandleRequest(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);
@@ -87,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/announce_begin_server.hpp b/src/core/thread/announce_begin_server.hpp
index aef72e2..a5aafd1 100644
--- a/src/core/thread/announce_begin_server.hpp
+++ b/src/core/thread/announce_begin_server.hpp
@@ -36,11 +36,11 @@
 
 #include "openthread-core-config.h"
 
-#include "coap/coap.hpp"
 #include "common/locator.hpp"
 #include "common/timer.hpp"
 #include "net/ip6_address.hpp"
 #include "thread/announce_sender.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -50,6 +50,8 @@
  */
 class AnnounceBeginServer : public AnnounceSenderBase
 {
+    friend class Tmf::Agent;
+
 public:
     /**
      * This constructor initializes the object.
@@ -72,14 +74,13 @@
     static constexpr uint16_t kDefaultPeriod = 1000;
     static constexpr uint16_t kDefaultJitter = 0;
 
-    static void HandleRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleRequest(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
     static void HandleTimer(Timer &aTimer);
-
-    Coap::Resource mAnnounceBegin;
 };
 
+DeclareTmfHandler(AnnounceBeginServer, kUriAnnounceBegin);
+
 /**
  * @}
  */
diff --git a/src/core/thread/announce_sender.cpp b/src/core/thread/announce_sender.cpp
index c500576..810939e 100644
--- a/src/core/thread/announce_sender.cpp
+++ b/src/core/thread/announce_sender.cpp
@@ -161,10 +161,7 @@
     SetJitter(kMaxJitter);
 }
 
-void AnnounceSender::UpdateOnReceivedAnnounce(void)
-{
-    mTrickleTimer.IndicateConsistent();
-}
+void AnnounceSender::UpdateOnReceivedAnnounce(void) { mTrickleTimer.IndicateConsistent(); }
 
 void AnnounceSender::Stop(void)
 {
@@ -173,15 +170,9 @@
     LogInfo("Stopped");
 }
 
-void AnnounceSender::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<AnnounceSender>().AnnounceSenderBase::HandleTimer();
-}
+void AnnounceSender::HandleTimer(Timer &aTimer) { aTimer.Get<AnnounceSender>().AnnounceSenderBase::HandleTimer(); }
 
-void AnnounceSender::HandleTrickleTimer(TrickleTimer &aTimer)
-{
-    aTimer.Get<AnnounceSender>().HandleTrickleTimer();
-}
+void AnnounceSender::HandleTrickleTimer(TrickleTimer &aTimer) { aTimer.Get<AnnounceSender>().HandleTrickleTimer(); }
 
 void AnnounceSender::HandleTrickleTimer(void)
 {
@@ -259,7 +250,7 @@
 
     SetChannelMask(channelMask);
     SetPeriod(kTxInterval / channelMask.GetNumberOfChannels());
-    LogInfo("ChannelMask:%s, period:%u", GetChannelMask().ToString().AsCString(), GetPeriod());
+    LogInfo("ChannelMask:%s, period:%lu", GetChannelMask().ToString().AsCString(), ToUlong(GetPeriod()));
 
     // When channel mask is changed, we also check and update the PAN
     // channel. This handles the case where `ThreadChannelChanged` event
diff --git a/src/core/thread/anycast_locator.cpp b/src/core/thread/anycast_locator.cpp
index a8989cd..88a02d4 100644
--- a/src/core/thread/anycast_locator.cpp
+++ b/src/core/thread/anycast_locator.cpp
@@ -46,31 +46,23 @@
 
 AnycastLocator::AnycastLocator(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mCallback(nullptr)
-    , mContext(nullptr)
-#if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_SEND_RESPONSE
-    , mAnycastLocate(UriPath::kAnycastLocate, HandleAnycastLocate, this)
-#endif
 
 {
-#if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_SEND_RESPONSE
-    Get<Tmf::Agent>().AddResource(mAnycastLocate);
-#endif
 }
 
-Error AnycastLocator::Locate(const Ip6::Address &aAnycastAddress, Callback aCallback, void *aContext)
+Error AnycastLocator::Locate(const Ip6::Address &aAnycastAddress, LocatorCallback aCallback, void *aContext)
 {
     Error            error   = kErrorNone;
-    Coap::Message *  message = nullptr;
+    Coap::Message   *message = nullptr;
     Tmf::MessageInfo messageInfo(GetInstance());
 
     VerifyOrExit((aCallback != nullptr) && Get<Mle::Mle>().IsAnycastLocator(aAnycastAddress),
                  error = kErrorInvalidArgs);
 
-    message = Get<Tmf::Agent>().NewConfirmablePostMessage(UriPath::kAnycastLocate);
+    message = Get<Tmf::Agent>().NewConfirmablePostMessage(kUriAnycastLocate);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
-    if (mCallback != nullptr)
+    if (mCallback.IsSet())
     {
         IgnoreError(Get<Tmf::Agent>().AbortTransaction(HandleResponse, this));
     }
@@ -79,16 +71,15 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, HandleResponse, this));
 
-    mCallback = aCallback;
-    mContext  = aContext;
+    mCallback.Set(aCallback, aContext);
 
 exit:
     FreeMessageOnError(message, error);
     return error;
 }
 
-void AnycastLocator::HandleResponse(void *               aContext,
-                                    otMessage *          aMessage,
+void AnycastLocator::HandleResponse(void                *aContext,
+                                    otMessage           *aMessage,
                                     const otMessageInfo *aMessageInfo,
                                     Error                aError)
 {
@@ -118,29 +109,25 @@
     address = &meshLocalAddress;
 
 exit:
-    if (mCallback != nullptr)
+    if (mCallback.IsSet())
     {
-        Callback callback = mCallback;
+        Callback<LocatorCallback> callbackCopy = mCallback;
 
-        mCallback = nullptr;
-        callback(mContext, aError, address, rloc16);
+        mCallback.Clear();
+        callbackCopy.Invoke(aError, address, rloc16);
     }
 }
 
 #if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_SEND_RESPONSE
 
-void AnycastLocator::HandleAnycastLocate(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<AnycastLocator *>(aContext)->HandleAnycastLocate(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void AnycastLocator::HandleAnycastLocate(const Coap::Message &aRequest, const Ip6::MessageInfo &aMessageInfo)
+template <>
+void AnycastLocator::HandleTmf<kUriAnycastLocate>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Coap::Message *message = nullptr;
 
-    VerifyOrExit(aRequest.IsConfirmablePostRequest());
+    VerifyOrExit(aMessage.IsConfirmablePostRequest());
 
-    message = Get<Tmf::Agent>().NewResponseMessage(aRequest);
+    message = Get<Tmf::Agent>().NewResponseMessage(aMessage);
     VerifyOrExit(message != nullptr);
 
     SuccessOrExit(Tlv::Append<ThreadMeshLocalEidTlv>(*message, Get<Mle::Mle>().GetMeshLocal64().GetIid()));
diff --git a/src/core/thread/anycast_locator.hpp b/src/core/thread/anycast_locator.hpp
index 5ae937f..cba969d 100644
--- a/src/core/thread/anycast_locator.hpp
+++ b/src/core/thread/anycast_locator.hpp
@@ -38,10 +38,11 @@
 
 #if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE
 
-#include "coap/coap.hpp"
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "net/ip6_address.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -54,12 +55,14 @@
  */
 class AnycastLocator : public InstanceLocator, private NonCopyable
 {
+    friend class Tmf::Agent;
+
 public:
     /**
      * This function pointer type defines the callback to notify the outcome of a request.
      *
      */
-    typedef otThreadAnycastLocatorCallback Callback;
+    typedef otThreadAnycastLocatorCallback LocatorCallback;
 
     /**
      * This constructor initializes the `AnycastLocator` object.
@@ -84,7 +87,7 @@
      * @retval kErrorInvalidArgs  The @p aAnycastAddress is not a valid anycast address or @p aCallback is `nullptr`.
      *
      */
-    Error Locate(const Ip6::Address &aAnycastAddress, Callback aCallback, void *aContext);
+    Error Locate(const Ip6::Address &aAnycastAddress, LocatorCallback aCallback, void *aContext);
 
     /**
      * This method indicates whether an earlier request is in progress.
@@ -92,25 +95,22 @@
      * @returns TRUE if an earlier request is in progress, FALSE otherwise.
      *
      */
-    bool IsInProgress(void) const { return (mCallback != nullptr); }
+    bool IsInProgress(void) const { return mCallback.IsSet(); }
 
 private:
     static void HandleResponse(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo, Error aError);
 
     void HandleResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aError);
 
-#if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_SEND_RESPONSE
-    static void HandleAnycastLocate(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleAnycastLocate(const Coap::Message &aRequest, const Ip6::MessageInfo &aMessageInfo);
-#endif
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    Callback mCallback;
-    void *   mContext;
-#if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_SEND_RESPONSE
-    Coap::Resource mAnycastLocate;
-#endif
+    Callback<LocatorCallback> mCallback;
 };
 
+#if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_SEND_RESPONSE
+DeclareTmfHandler(AnycastLocator, kUriAnycastLocate);
+#endif
+
 } // namespace ot
 
 #endif // OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE
diff --git a/src/core/utils/child_supervision.cpp b/src/core/thread/child_supervision.cpp
similarity index 80%
rename from src/core/utils/child_supervision.cpp
rename to src/core/thread/child_supervision.cpp
index b0f45bd..02ba64a 100644
--- a/src/core/utils/child_supervision.cpp
+++ b/src/core/thread/child_supervision.cpp
@@ -41,29 +41,19 @@
 #include "thread/thread_netif.hpp"
 
 namespace ot {
-namespace Utils {
 
 RegisterLogModule("ChildSupervsn");
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-
 #if OPENTHREAD_FTD
 
 ChildSupervisor::ChildSupervisor(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mSupervisionInterval(kDefaultSupervisionInterval)
 {
 }
 
-void ChildSupervisor::SetSupervisionInterval(uint16_t aInterval)
-{
-    mSupervisionInterval = aInterval;
-    CheckState();
-}
-
 Child *ChildSupervisor::GetDestination(const Message &aMessage) const
 {
-    Child *  child = nullptr;
+    Child   *child = nullptr;
     uint16_t childIndex;
 
     VerifyOrExit(aMessage.GetType() == Message::kTypeSupervision);
@@ -93,7 +83,7 @@
     childIndex = Get<ChildTable>().GetChildIndex(aChild);
     SuccessOrExit(message->Append(childIndex));
 
-    SuccessOrExit(Get<ThreadNetif>().SendMessage(*message));
+    SuccessOrExit(Get<MeshForwarder>().SendMessage(*message));
     message = nullptr;
 
     LogInfo("Sending supervision message to child 0x%04x", aChild.GetRloc16());
@@ -102,18 +92,20 @@
     FreeMessage(message);
 }
 
-void ChildSupervisor::UpdateOnSend(Child &aChild)
-{
-    aChild.ResetSecondsSinceLastSupervision();
-}
+void ChildSupervisor::UpdateOnSend(Child &aChild) { aChild.ResetSecondsSinceLastSupervision(); }
 
 void ChildSupervisor::HandleTimeTick(void)
 {
     for (Child &child : Get<ChildTable>().Iterate(Child::kInStateValid))
     {
+        if (child.IsRxOnWhenIdle() || (child.GetSupervisionInterval() == 0))
+        {
+            continue;
+        }
+
         child.IncrementSecondsSinceLastSupervision();
 
-        if ((child.GetSecondsSinceLastSupervision() >= mSupervisionInterval) && !child.IsRxOnWhenIdle())
+        if (child.GetSecondsSinceLastSupervision() >= child.GetSupervisionInterval())
         {
             SendMessage(child);
         }
@@ -122,14 +114,11 @@
 
 void ChildSupervisor::CheckState(void)
 {
-    bool shouldRun = false;
+    // Child Supervision should run if Thread MLE operation is
+    // enabled, and there is at least one "valid" child in the
+    // child table.
 
-    // Child Supervision should run if `mSupervisionInterval` is not
-    // zero, Thread MLE operation is enabled, and there is at least one
-    // "valid" child in the child table.
-
-    shouldRun = ((mSupervisionInterval != 0) && !Get<Mle::MleRouter>().IsDisabled() &&
-                 Get<ChildTable>().HasChildren(Child::kInStateValid));
+    bool shouldRun = (!Get<Mle::Mle>().IsDisabled() && Get<ChildTable>().HasChildren(Child::kInStateValid));
 
     if (shouldRun && !Get<TimeTicker>().IsReceiverRegistered(TimeTicker::kChildSupervisor))
     {
@@ -157,25 +146,39 @@
 SupervisionListener::SupervisionListener(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mTimeout(0)
-    , mTimer(aInstance, SupervisionListener::HandleTimer)
+    , mInterval(kDefaultInterval)
+    , mTimer(aInstance)
 {
     SetTimeout(kDefaultTimeout);
 }
 
-void SupervisionListener::Start(void)
-{
-    RestartTimer();
-}
+void SupervisionListener::Start(void) { RestartTimer(); }
 
-void SupervisionListener::Stop(void)
+void SupervisionListener::Stop(void) { mTimer.Stop(); }
+
+void SupervisionListener::SetInterval(uint16_t aInterval)
 {
-    mTimer.Stop();
+    VerifyOrExit(mInterval != aInterval);
+
+    LogInfo("Interval: %u -> %u", mInterval, aInterval);
+
+    mInterval = aInterval;
+
+    if (Get<Mle::Mle>().IsChild())
+    {
+        IgnoreError(Get<Mle::Mle>().SendChildUpdateRequest());
+    }
+
+exit:
+    return;
 }
 
 void SupervisionListener::SetTimeout(uint16_t aTimeout)
 {
     if (mTimeout != aTimeout)
     {
+        LogInfo("Timeout: %u -> %u", mTimeout, aTimeout);
+
         mTimeout = aTimeout;
         RestartTimer();
     }
@@ -206,16 +209,12 @@
     }
 }
 
-void SupervisionListener::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<SupervisionListener>().HandleTimer();
-}
-
 void SupervisionListener::HandleTimer(void)
 {
     VerifyOrExit(Get<Mle::MleRouter>().IsChild() && !Get<MeshForwarder>().GetRxOnWhenIdle());
 
-    LogWarn("Supervision timeout. No frame from parent in %d sec", mTimeout);
+    LogWarn("Supervision timeout. No frame from parent in %u sec", mTimeout);
+    mCounter++;
 
     IgnoreError(Get<Mle::MleRouter>().SendChildUpdateRequest());
 
@@ -223,7 +222,4 @@
     RestartTimer();
 }
 
-#endif // #if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-
-} // namespace Utils
 } // namespace ot
diff --git a/src/core/utils/child_supervision.hpp b/src/core/thread/child_supervision.hpp
similarity index 87%
rename from src/core/utils/child_supervision.hpp
rename to src/core/thread/child_supervision.hpp
index 30b26f1..c56291f 100644
--- a/src/core/utils/child_supervision.hpp
+++ b/src/core/thread/child_supervision.hpp
@@ -53,8 +53,6 @@
 class ThreadNetif;
 class Child;
 
-namespace Utils {
-
 /**
  *
  * Child supervision feature provides a mechanism for parent
@@ -85,8 +83,6 @@
  *
  */
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-
 #if OPENTHREAD_FTD
 
 /**
@@ -120,25 +116,6 @@
     void Stop(void);
 
     /**
-     * This method sets the supervision interval.
-     *
-     * Setting the supervision interval to a non-zero value will ensure to start the supervision process (if not
-     * already started).
-     *
-     * @param[in] aInterval If non-zero, the desired supervision interval (in seconds), zero to disable supervision.
-     *
-     */
-    void SetSupervisionInterval(uint16_t aInterval);
-
-    /**
-     * This method returns the supervision interval.
-     *
-     * @returns  The current supervision interval (seconds), or zero if supervision is disabled.
-     *
-     */
-    uint16_t GetSupervisionInterval(void) const { return mSupervisionInterval; }
-
-    /**
      * This method returns the destination for a supervision message.
      *
      * @param[in] aMessage The message for which to get the destination.
@@ -165,8 +142,6 @@
     void CheckState(void);
     void HandleTimeTick(void);
     void HandleNotifierEvents(Events aEvents);
-
-    uint16_t mSupervisionInterval;
 };
 
 #endif // #if OPENTHREAD_FTD
@@ -199,6 +174,22 @@
     void Stop(void);
 
     /**
+     * This method sets the supervision interval.
+     *
+     * @param[in] aInterval If non-zero, the desired supervision interval (in seconds), zero to disable supervision.
+     *
+     */
+    void SetInterval(uint16_t aInterval);
+
+    /**
+     * This method returns the supervision interval.
+     *
+     * @returns  The current supervision interval (seconds), or zero if supervision is disabled.
+     *
+     */
+    uint16_t GetInterval(void) const { return mInterval; }
+
+    /**
      * This method sets the supervision check timeout (in seconds).
      *
      * If the child does not hear from its parent within the given check timeout interval, it initiates the re-attach
@@ -222,6 +213,21 @@
     uint16_t GetTimeout(void) const { return mTimeout; }
 
     /**
+     * This method returns the value of supervision check timeout failure counter.
+     *
+     * The counter tracks the number of supervision check failures on the child. It is incremented when the child does
+     * not hear from its parent within the specified check timeout interval.
+     *
+     */
+    uint16_t GetCounter(void) const { return mCounter; }
+
+    /**
+     * This method reset the supervision check timeout failure counter.
+     *
+     */
+    void ResetCounter(void) { mCounter = 0; }
+
+    /**
      * This method updates the supervision listener state. It informs the listener of a received frame.
      *
      * @param[in]   aSourceAddress    The source MAC address of the received frame
@@ -231,24 +237,20 @@
     void UpdateOnReceive(const Mac::Address &aSourceAddress, bool aIsSecure);
 
 private:
-    static constexpr uint16_t kDefaultTimeout = OPENTHREAD_CONFIG_CHILD_SUPERVISION_CHECK_TIMEOUT; // (seconds)
+    static constexpr uint16_t kDefaultTimeout  = OPENTHREAD_CONFIG_CHILD_SUPERVISION_CHECK_TIMEOUT; // (seconds)
+    static constexpr uint16_t kDefaultInterval = OPENTHREAD_CONFIG_CHILD_SUPERVISION_INTERVAL;      // (seconds)
 
-    void        RestartTimer(void);
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void RestartTimer(void);
+    void HandleTimer(void);
 
-    uint16_t   mTimeout;
-    TimerMilli mTimer;
+    using ListenerTimer = TimerMilliIn<SupervisionListener, &SupervisionListener::HandleTimer>;
+
+    uint16_t      mTimeout;
+    uint16_t      mInterval;
+    uint16_t      mCounter;
+    ListenerTimer mTimer;
 };
 
-#endif // #if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-
-/**
- * @}
- *
- */
-
-} // namespace Utils
 } // namespace ot
 
 #endif // CHILD_SUPERVISION_HPP_
diff --git a/src/core/thread/child_table.cpp b/src/core/thread/child_table.cpp
index 9eca695..e5ec209 100644
--- a/src/core/thread/child_table.cpp
+++ b/src/core/thread/child_table.cpp
@@ -184,12 +184,12 @@
 Error ChildTable::GetChildInfoById(uint16_t aChildId, Child::Info &aChildInfo)
 {
     Error    error = kErrorNone;
-    Child *  child;
+    Child   *child;
     uint16_t rloc16;
 
     if ((aChildId & ~Mle::kMaxChildId) != 0)
     {
-        aChildId = Mle::Mle::ChildIdFromRloc16(aChildId);
+        aChildId = Mle::ChildIdFromRloc16(aChildId);
     }
 
     rloc16 = Get<Mac::Mac>().GetShortAddress() | aChildId;
@@ -246,7 +246,7 @@
         child->SetDeviceMode(Mle::DeviceMode(childInfo.GetMode()));
         child->SetState(Neighbor::kStateRestored);
         child->SetLastHeard(TimerMilli::GetNow());
-        child->SetVersion(static_cast<uint8_t>(childInfo.GetVersion()));
+        child->SetVersion(childInfo.GetVersion());
         Get<IndirectSender>().SetChildUseShortAddress(*child, true);
         Get<NeighborTable>().Signal(NeighborTable::kChildAdded, *child);
         numChildren++;
diff --git a/src/core/thread/csl_tx_scheduler.cpp b/src/core/thread/csl_tx_scheduler.cpp
index 15beb59..9d86408 100644
--- a/src/core/thread/csl_tx_scheduler.cpp
+++ b/src/core/thread/csl_tx_scheduler.cpp
@@ -46,7 +46,7 @@
 
 inline Error CslTxScheduler::Callbacks::PrepareFrameForChild(Mac::TxFrame &aFrame,
                                                              FrameContext &aContext,
-                                                             Child &       aChild)
+                                                             Child        &aChild)
 {
     return Get<IndirectSender>().PrepareFrameForChild(aFrame, aContext, aChild);
 }
@@ -54,7 +54,7 @@
 inline void CslTxScheduler::Callbacks::HandleSentFrameToChild(const Mac::TxFrame &aFrame,
                                                               const FrameContext &aContext,
                                                               Error               aError,
-                                                              Child &             aChild)
+                                                              Child              &aChild)
 {
     Get<IndirectSender>().HandleSentFrameToChild(aFrame, aContext, aError, aChild);
 }
@@ -90,6 +90,7 @@
     {
         // `Mac` has already started the CSL tx, so wait for tx done callback
         // to call `RescheduleCslTx`
+        mCslTxChild->ResetCslTxAttempts();
         mCslTxChild                      = nullptr;
         mFrameContext.mMessageNextOffset = 0;
     }
@@ -122,7 +123,7 @@
 void CslTxScheduler::RescheduleCslTx(void)
 {
     uint32_t minDelayTime = Time::kMaxDuration;
-    Child *  bestChild    = nullptr;
+    Child   *bestChild    = nullptr;
 
     for (Child &child : Get<ChildTable>().Iterate(Child::kInStateAnyExceptInvalid))
     {
@@ -134,7 +135,7 @@
             continue;
         }
 
-        delay = GetNextCslTransmissionDelay(child, cslTxDelay);
+        delay = GetNextCslTransmissionDelay(child, cslTxDelay, mCslFrameRequestAheadUs);
 
         if (delay < minDelayTime)
         {
@@ -151,18 +152,24 @@
     mCslTxChild = bestChild;
 }
 
-uint32_t CslTxScheduler::GetNextCslTransmissionDelay(const Child &aChild, uint32_t &aDelayFromLastRx) const
+uint32_t CslTxScheduler::GetNextCslTransmissionDelay(const Child &aChild,
+                                                     uint32_t    &aDelayFromLastRx,
+                                                     uint32_t     aAheadUs) const
 {
-    uint64_t radioNow      = otPlatRadioGetNow(&GetInstance());
-    uint32_t periodInUs    = aChild.GetCslPeriod() * kUsPerTenSymbols;
-    uint64_t firstTxWindow = aChild.GetLastRxTimestamp() + aChild.GetCslPhase() * kUsPerTenSymbols;
-    uint64_t nextTxWindow  = radioNow - (radioNow % periodInUs) + (firstTxWindow % periodInUs);
+    uint64_t radioNow   = otPlatRadioGetNow(&GetInstance());
+    uint32_t periodInUs = aChild.GetCslPeriod() * kUsPerTenSymbols;
+    uint64_t firstTxWindow =
+        aChild.GetLastRxTimestamp() - kRadioHeaderShrDuration + aChild.GetCslPhase() * kUsPerTenSymbols;
+    uint64_t nextTxWindow = radioNow - (radioNow % periodInUs) + (firstTxWindow % periodInUs);
 
-    while (nextTxWindow < radioNow + mCslFrameRequestAheadUs) nextTxWindow += periodInUs;
+    while (nextTxWindow < radioNow + aAheadUs)
+    {
+        nextTxWindow += periodInUs;
+    }
 
     aDelayFromLastRx = static_cast<uint32_t>(nextTxWindow - aChild.GetLastRxTimestamp());
 
-    return static_cast<uint32_t>(nextTxWindow - radioNow - mCslFrameRequestAheadUs);
+    return static_cast<uint32_t>(nextTxWindow - radioNow - aAheadUs);
 }
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
@@ -171,6 +178,7 @@
 {
     Mac::TxFrame *frame = nullptr;
     uint32_t      txDelay;
+    uint32_t      delay;
 
     VerifyOrExit(mCslTxChild != nullptr);
 
@@ -207,7 +215,30 @@
     frame->SetChannel(mCslTxChild->GetCslChannel() == 0 ? Get<Mac::Mac>().GetPanChannel()
                                                         : mCslTxChild->GetCslChannel());
 
-    GetNextCslTransmissionDelay(*mCslTxChild, txDelay);
+    if (frame->GetChannel() != Get<Mac::Mac>().GetPanChannel())
+    {
+        frame->SetRxChannelAfterTxDone(Get<Mac::Mac>().GetPanChannel());
+    }
+
+    delay = GetNextCslTransmissionDelay(*mCslTxChild, txDelay, /* aAheadUs */ 0);
+
+    // We make sure that delay is less than `mCslFrameRequestAheadUs`
+    // plus some guard time. Note that we used `mCslFrameRequestAheadUs`
+    // 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
+    // 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`.
+    //
+    // In general this check handles the case where `Mac` is busy with
+    // other operations and therefore late to start the CSL tx operation
+    // and by the time `HandleFrameRequest()` is invoked, we miss the
+    // current CSL window and move to the next window.
+
+    VerifyOrExit(delay <= mCslFrameRequestAheadUs + kFramePreparationGuardInterval, frame = nullptr);
+
     frame->SetTxDelay(txDelay);
     frame->SetTxDelayBaseTime(
         static_cast<uint32_t>(mCslTxChild->GetLastRxTimestamp())); // Only LSB part of the time is required.
@@ -219,10 +250,7 @@
 
 #else // OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
 
-Mac::TxFrame *CslTxScheduler::HandleFrameRequest(Mac::TxFrames &)
-{
-    return nullptr;
-}
+Mac::TxFrame *CslTxScheduler::HandleFrameRequest(Mac::TxFrames &) { return nullptr; }
 
 #endif // OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
 
@@ -239,7 +267,7 @@
     HandleSentFrame(aFrame, aError, *child);
 
 exit:
-    return;
+    RescheduleCslTx();
 }
 
 void CslTxScheduler::HandleSentFrame(const Mac::TxFrame &aFrame, Error aError, Child &aChild)
@@ -291,7 +319,6 @@
             }
         }
 
-        RescheduleCslTx();
         ExitNow();
 
     default:
diff --git a/src/core/thread/csl_tx_scheduler.hpp b/src/core/thread/csl_tx_scheduler.hpp
index d8e0368..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!");
     };
@@ -160,7 +160,7 @@
         void HandleSentFrameToChild(const Mac::TxFrame &aFrame,
                                     const FrameContext &aContext,
                                     Error               aError,
-                                    Child &             aChild);
+                                    Child              &aChild);
     };
     /**
      * This constructor initializes the CSL tx scheduler object.
@@ -187,10 +187,13 @@
     void Clear(void);
 
 private:
+    // 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);
     void RescheduleCslTx(void);
 
-    uint32_t GetNextCslTransmissionDelay(const Child &aChild, uint32_t &aDelayFromLastRx) const;
+    uint32_t GetNextCslTransmissionDelay(const Child &aChild, uint32_t &aDelayFromLastRx, uint32_t aAheadUs) const;
 
     // Callbacks from `Mac`
     Mac::TxFrame *HandleFrameRequest(Mac::TxFrames &aTxFrames);
@@ -199,8 +202,8 @@
     void HandleSentFrame(const Mac::TxFrame &aFrame, Error aError, Child &aChild);
 
     uint32_t                mCslFrameRequestAheadUs;
-    Child *                 mCslTxChild;
-    Message *               mCslTxMessage;
+    Child                  *mCslTxChild;
+    Message                *mCslTxMessage;
     Callbacks::FrameContext mFrameContext;
     Callbacks               mCallbacks;
 };
diff --git a/src/core/thread/discover_scanner.cpp b/src/core/thread/discover_scanner.cpp
index bf6d60f..288bbe7 100644
--- a/src/core/thread/discover_scanner.cpp
+++ b/src/core/thread/discover_scanner.cpp
@@ -40,15 +40,15 @@
 #include "thread/mesh_forwarder.hpp"
 #include "thread/mle.hpp"
 #include "thread/mle_router.hpp"
+#include "thread/version.hpp"
 
 namespace ot {
 namespace Mle {
 
 DiscoverScanner::DiscoverScanner(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mHandler(nullptr)
-    , mHandlerContext(nullptr)
-    , mTimer(aInstance, DiscoverScanner::HandleTimer)
+    , mScanDoneTask(aInstance)
+    , mTimer(aInstance)
     , mFilterIndexes()
     , mState(kStateIdle)
     , mScanChannel(0)
@@ -62,12 +62,12 @@
                                 uint16_t                aPanId,
                                 bool                    aJoiner,
                                 bool                    aEnableFiltering,
-                                const FilterIndexes *   aFilterIndexes,
+                                const FilterIndexes    *aFilterIndexes,
                                 Handler                 aCallback,
-                                void *                  aContext)
+                                void                   *aContext)
 {
     Error                           error   = kErrorNone;
-    Mle::TxMessage *                message = nullptr;
+    Mle::TxMessage                 *message = nullptr;
     Tlv                             tlv;
     Ip6::Address                    destination;
     MeshCoP::DiscoveryRequestTlv    discoveryRequest;
@@ -95,8 +95,7 @@
         }
     }
 
-    mHandler            = aCallback;
-    mHandlerContext     = aContext;
+    mCallback.Set(aCallback, aContext);
     mShouldRestorePanId = false;
     mScanChannels       = Get<Mac::Mac>().GetSupportedChannelMask();
 
@@ -153,6 +152,12 @@
     mScanChannel = Mac::ChannelMask::kChannelIteratorFirst;
     mState       = (mScanChannels.GetNextChannel(mScanChannel) == kErrorNone) ? kStateScanning : kStateScanDone;
 
+    // For rx-off-when-idle device, temporarily enable receiver during discovery procedure.
+    if (!Get<Mle>().IsDisabled() && !Get<Mle>().IsRxOnWhenIdle())
+    {
+        Get<MeshForwarder>().SetRxOnWhenIdle(true);
+    }
+
     Mle::Log(Mle::kMessageSend, Mle::kTypeDiscoveryRequest, destination);
 
 exit:
@@ -226,6 +231,12 @@
 
 void DiscoverScanner::HandleDiscoverComplete(void)
 {
+    // Restore Data Polling or CSL for rx-off-when-idle device.
+    if (!Get<Mle>().IsDisabled() && !Get<Mle>().IsRxOnWhenIdle())
+    {
+        Get<MeshForwarder>().SetRxOnWhenIdle(false);
+    }
+
     switch (mState)
     {
     case kStateIdle:
@@ -246,20 +257,18 @@
             mShouldRestorePanId = false;
         }
 
-        mState = kStateIdle;
-
-        if (mHandler)
-        {
-            mHandler(nullptr, mHandlerContext);
-        }
-
+        // Post the tasklet to change `mState` and invoke handler
+        // callback. This allows users to safely call OT APIs from
+        // the callback.
+        mScanDoneTask.Post();
         break;
     }
 }
 
-void DiscoverScanner::HandleTimer(Timer &aTimer)
+void DiscoverScanner::HandleScanDoneTask(void)
 {
-    aTimer.Get<DiscoverScanner>().HandleTimer();
+    mState = kStateIdle;
+    mCallback.InvokeIfSet(nullptr);
 }
 
 void DiscoverScanner::HandleTimer(void)
@@ -287,13 +296,13 @@
 void DiscoverScanner::HandleDiscoveryResponse(Mle::RxInfo &aRxInfo) const
 {
     Error                         error    = kErrorNone;
-    const ThreadLinkInfo *        linkInfo = aRxInfo.mMessageInfo.GetThreadLinkInfo();
-    Tlv                           tlv;
+    const ThreadLinkInfo         *linkInfo = aRxInfo.mMessageInfo.GetThreadLinkInfo();
     MeshCoP::Tlv                  meshcopTlv;
     MeshCoP::DiscoveryResponseTlv discoveryResponse;
     MeshCoP::NetworkNameTlv       networkName;
     ScanResult                    result;
     uint16_t                      offset;
+    uint16_t                      length;
     uint16_t                      end;
     bool                          didCheckSteeringData = false;
 
@@ -302,11 +311,8 @@
     VerifyOrExit(mState == kStateScanning, error = kErrorDrop);
 
     // Find MLE Discovery TLV
-    VerifyOrExit(Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kDiscovery, offset) == kErrorNone, error = kErrorParse);
-    IgnoreError(aRxInfo.mMessage.Read(offset, tlv));
-
-    offset += sizeof(tlv);
-    end = offset + tlv.GetLength();
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kDiscovery, offset, length));
+    end = offset + length;
 
     memset(&result, 0, sizeof(result));
     result.mDiscover = true;
@@ -357,7 +363,7 @@
 
                 steeringData.Init(dataLength);
 
-                SuccessOrExit(error = Tlv::ReadTlv(aRxInfo.mMessage, offset, steeringData.GetData(), dataLength));
+                SuccessOrExit(error = Tlv::ReadTlvValue(aRxInfo.mMessage, offset, steeringData.GetData(), dataLength));
 
                 if (mEnableFiltering)
                 {
@@ -382,10 +388,7 @@
 
     VerifyOrExit(!mEnableFiltering || didCheckSteeringData);
 
-    if (mHandler)
-    {
-        mHandler(&result, mHandlerContext);
-    }
+    mCallback.InvokeIfSet(&result);
 
 exit:
     Mle::LogProcessError(Mle::kTypeDiscoveryResponse, error);
diff --git a/src/core/thread/discover_scanner.hpp b/src/core/thread/discover_scanner.hpp
index 061aa2f..ac5ac52 100644
--- a/src/core/thread/discover_scanner.hpp
+++ b/src/core/thread/discover_scanner.hpp
@@ -36,8 +36,10 @@
 
 #include "openthread-core-config.h"
 
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
+#include "common/tasklet.hpp"
 #include "common/timer.hpp"
 #include "mac/channel_mask.hpp"
 #include "mac/mac.hpp"
@@ -126,9 +128,9 @@
                    Mac::PanId              aPanId,
                    bool                    aJoiner,
                    bool                    aEnableFiltering,
-                   const FilterIndexes *   aFilterIndexes,
+                   const FilterIndexes    *aFilterIndexes,
                    Handler                 aCallback,
-                   void *                  aContext);
+                   void                   *aContext);
 
     /**
      * This method indicates whether or not an MLE Thread Discovery Scan is currently in progress.
@@ -169,22 +171,25 @@
     // Methods used from `Mle`
     void HandleDiscoveryResponse(Mle::RxInfo &aRxInfo) const;
 
-    void        HandleDiscoverComplete(void);
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleDiscoverComplete(void);
+    void HandleScanDoneTask(void);
+    void HandleTimer(void);
 
-    Handler          mHandler;
-    void *           mHandlerContext;
-    TimerMilli       mTimer;
-    FilterIndexes    mFilterIndexes;
-    Mac::ChannelMask mScanChannels;
-    State            mState;
-    uint32_t         mOui;
-    uint8_t          mScanChannel;
-    uint8_t          mAdvDataLength;
-    uint8_t          mAdvData[MeshCoP::JoinerAdvertisementTlv::kAdvDataMaxLength];
-    bool             mEnableFiltering : 1;
-    bool             mShouldRestorePanId : 1;
+    using ScanTimer    = TimerMilliIn<DiscoverScanner, &DiscoverScanner::HandleTimer>;
+    using ScanDoneTask = TaskletIn<DiscoverScanner, &DiscoverScanner::HandleScanDoneTask>;
+
+    Callback<Handler> mCallback;
+    ScanDoneTask      mScanDoneTask;
+    ScanTimer         mTimer;
+    FilterIndexes     mFilterIndexes;
+    Mac::ChannelMask  mScanChannels;
+    State             mState;
+    uint32_t          mOui;
+    uint8_t           mScanChannel;
+    uint8_t           mAdvDataLength;
+    uint8_t           mAdvData[MeshCoP::JoinerAdvertisementTlv::kAdvDataMaxLength];
+    bool              mEnableFiltering : 1;
+    bool              mShouldRestorePanId : 1;
 };
 
 } // namespace Mle
diff --git a/src/core/thread/dua_manager.cpp b/src/core/thread/dua_manager.cpp
index 108f07a..53faa17 100644
--- a/src/core/thread/dua_manager.cpp
+++ b/src/core/thread/dua_manager.cpp
@@ -54,8 +54,7 @@
 
 DuaManager::DuaManager(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mRegistrationTask(aInstance, DuaManager::HandleRegistrationTask)
-    , mDuaNotification(UriPath::kDuaRegistrationNotify, &DuaManager::HandleDuaNotification, this)
+    , mRegistrationTask(aInstance)
     , mIsDuaPending(false)
 #if OPENTHREAD_CONFIG_DUA_ENABLE
     , mDuaState(kNotExist)
@@ -77,8 +76,6 @@
     mChildDuaMask.Clear();
     mChildDuaRegisteredMask.Clear();
 #endif
-
-    Get<Tmf::Agent>().AddResource(mDuaNotification);
 }
 
 void DuaManager::HandleDomainPrefixUpdate(BackboneRouter::Leader::DomainPrefixState aState)
@@ -108,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;
@@ -276,7 +273,7 @@
 void DuaManager::UpdateReregistrationDelay(void)
 {
     uint16_t               delay = 0;
-    otBackboneRouterConfig config;
+    BackboneRouter::Config config;
 
     VerifyOrExit(Get<BackboneRouter::Leader>().GetConfig(config) == kErrorNone);
 
@@ -308,6 +305,19 @@
 {
     Mle::MleRouter &mle = Get<Mle::MleRouter>();
 
+#if OPENTHREAD_CONFIG_DUA_ENABLE
+    if (aEvents.Contains(kEventThreadNetdataChanged))
+    {
+        Lowpan::Context context;
+        // Remove a stale DUA address if any.
+        if (Get<ThreadNetif>().HasUnicastAddress(Get<DuaManager>().GetDomainUnicastAddress()) &&
+            (Get<NetworkData::Leader>().GetContext(Get<DuaManager>().GetDomainUnicastAddress(), context) != kErrorNone))
+        {
+            RemoveDomainUnicastAddress();
+        }
+    }
+#endif
+
     VerifyOrExit(mle.IsAttached(), mDelay.mValue = 0);
 
     if (aEvents.Contains(kEventThreadRoleChanged))
@@ -341,14 +351,19 @@
     return;
 }
 
-void DuaManager::HandleBackboneRouterPrimaryUpdate(BackboneRouter::Leader::State               aState,
-                                                   const BackboneRouter::BackboneRouterConfig &aConfig)
+void DuaManager::HandleBackboneRouterPrimaryUpdate(BackboneRouter::Leader::State aState,
+                                                   const BackboneRouter::Config &aConfig)
 {
     OT_UNUSED_VARIABLE(aConfig);
 
     if (aState == BackboneRouter::Leader::kStateAdded || aState == BackboneRouter::Leader::kStateToTriggerRereg)
     {
-        UpdateReregistrationDelay();
+#if OPENTHREAD_CONFIG_DUA_ENABLE
+        if (Get<Mle::Mle>().IsFullThreadDevice() || Get<Mle::Mle>().GetParent().IsThreadVersion1p1())
+#endif
+        {
+            UpdateReregistrationDelay();
+        }
     }
 }
 
@@ -402,11 +417,6 @@
     UpdateTimeTickerRegistration();
 }
 
-void DuaManager::HandleRegistrationTask(Tasklet &aTasklet)
-{
-    aTasklet.Get<DuaManager>().PerformNextRegistration();
-}
-
 void DuaManager::UpdateTimeTickerRegistration(void)
 {
     if (mDelay.mValue == 0)
@@ -422,8 +432,8 @@
 void DuaManager::PerformNextRegistration(void)
 {
     Error            error   = kErrorNone;
-    Mle::MleRouter & mle     = Get<Mle::MleRouter>();
-    Coap::Message *  message = nullptr;
+    Mle::MleRouter  &mle     = Get<Mle::MleRouter>();
+    Coap::Message   *message = nullptr;
     Tmf::MessageInfo messageInfo(GetInstance());
     Ip6::Address     dua;
 
@@ -455,7 +465,7 @@
     }
 
     // Prepare DUA.req
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kDuaRegistrationRequest);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriDuaRegistrationRequest);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
 #if OPENTHREAD_CONFIG_DUA_ENABLE
@@ -473,7 +483,7 @@
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
         uint32_t            lastTransactionTime;
         const Ip6::Address *duaPtr = nullptr;
-        Child *             child  = nullptr;
+        Child              *child  = nullptr;
 
         OT_ASSERT(mChildIndexDuaRegistering == Mle::kMaxChildren);
 
@@ -531,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)
@@ -543,8 +553,8 @@
     FreeMessageOnError(message, error);
 }
 
-void DuaManager::HandleDuaResponse(void *               aContext,
-                                   otMessage *          aMessage,
+void DuaManager::HandleDuaResponse(void                *aContext,
+                                   otMessage           *aMessage,
                                    const otMessageInfo *aMessageInfo,
                                    Error                aResult)
 {
@@ -582,14 +592,11 @@
         mRegistrationTask.Post();
     }
 
-    LogInfo("Received DUA.rsp: %s", ErrorToString(error));
+    LogInfo("Received %s response: %s", UriToString<kUriDuaRegistrationRequest>(), ErrorToString(error));
 }
 
-void DuaManager::HandleDuaNotification(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<DuaManager *>(aContext)->HandleDuaNotification(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-void DuaManager::HandleDuaNotification(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <>
+void DuaManager::HandleTmf<kUriDuaRegistrationNotify>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     OT_UNUSED_VARIABLE(aMessageInfo);
 
@@ -599,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: %d", ErrorToString(error));
+    LogInfo("Received %s: %s", UriToString<kUriDuaRegistrationNotify>(), ErrorToString(error));
 }
 
 Error DuaManager::ProcessDuaResponse(Coap::Message &aMessage)
@@ -662,7 +669,7 @@
 #endif
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
     {
-        Child *  child = nullptr;
+        Child   *child = nullptr;
         uint16_t childIndex;
 
         for (Child &iter : Get<ChildTable>().Iterate(Child::kInStateValid))
@@ -718,15 +725,15 @@
 }
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
-void DuaManager::SendAddressNotification(Ip6::Address &             aAddress,
+void DuaManager::SendAddressNotification(Ip6::Address              &aAddress,
                                          ThreadStatusTlv::DuaStatus aStatus,
-                                         const Child &              aChild)
+                                         const Child               &aChild)
 {
-    Coap::Message *  message = nullptr;
+    Coap::Message   *message = nullptr;
     Tmf::MessageInfo messageInfo(GetInstance());
     Error            error;
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kDuaRegistrationNotify);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriDuaRegistrationNotify);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = Tlv::Append<ThreadStatusTlv>(*message, aStatus));
@@ -736,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:
 
@@ -745,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));
     }
 }
 
@@ -778,8 +786,6 @@
         mChildDuaMask.Set(childIndex, true);
         mChildDuaRegisteredMask.Set(childIndex, false);
     }
-
-    return;
 }
 #endif // OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
 
diff --git a/src/core/thread/dua_manager.hpp b/src/core/thread/dua_manager.hpp
index 45d7807..d8df6c6 100644
--- a/src/core/thread/dua_manager.hpp
+++ b/src/core/thread/dua_manager.hpp
@@ -47,7 +47,6 @@
 #endif
 
 #include "backbone_router/bbr_leader.hpp"
-#include "coap/coap.hpp"
 #include "coap/coap_message.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
@@ -58,6 +57,7 @@
 #include "common/timer.hpp"
 #include "net/netif.hpp"
 #include "thread/thread_tlvs.hpp"
+#include "thread/tmf.hpp"
 #include "thread/topology.hpp"
 
 namespace ot {
@@ -84,6 +84,7 @@
 {
     friend class ot::Notifier;
     friend class ot::TimeTicker;
+    friend class Tmf::Agent;
 
 public:
     /**
@@ -109,8 +110,7 @@
      * @param[in]  aConfig  The Primary Backbone Router service.
      *
      */
-    void HandleBackboneRouterPrimaryUpdate(BackboneRouter::Leader::State               aState,
-                                           const BackboneRouter::BackboneRouterConfig &aConfig);
+    void HandleBackboneRouterPrimaryUpdate(BackboneRouter::Leader::State aState, const BackboneRouter::Config &aConfig);
 
 #if OPENTHREAD_CONFIG_DUA_ENABLE
 
@@ -195,18 +195,15 @@
 
     void HandleTimeTick(void);
 
-    static void HandleRegistrationTask(Tasklet &aTasklet);
-
     void UpdateTimeTickerRegistration(void);
 
-    static void HandleDuaResponse(void *               aContext,
-                                  otMessage *          aMessage,
+    static void HandleDuaResponse(void                *aContext,
+                                  otMessage           *aMessage,
                                   const otMessageInfo *aMessageInfo,
                                   Error                aResult);
     void        HandleDuaResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult);
 
-    static void HandleDuaNotification(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleDuaNotification(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
     Error ProcessDuaResponse(Coap::Message &aMessage);
 
@@ -214,10 +211,11 @@
     void UpdateReregistrationDelay(void);
     void UpdateCheckDelay(uint8_t aDelay);
 
-    Tasklet        mRegistrationTask;
-    Coap::Resource mDuaNotification;
-    Ip6::Address   mRegisteringDua;
-    bool           mIsDuaPending : 1;
+    using RegistrationTask = TaskletIn<DuaManager, &DuaManager::PerformNextRegistration>;
+
+    RegistrationTask mRegistrationTask;
+    Ip6::Address     mRegisteringDua;
+    bool             mIsDuaPending : 1;
 
 #if OPENTHREAD_CONFIG_DUA_ENABLE
     enum DuaState : uint8_t
@@ -258,6 +256,8 @@
 #endif
 };
 
+DeclareTmfHandler(DuaManager, kUriDuaRegistrationNotify);
+
 } // namespace ot
 
 #endif // OPENTHREAD_CONFIG_DUA_ENABLE || (OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE)
diff --git a/src/core/thread/energy_scan_server.cpp b/src/core/thread/energy_scan_server.cpp
index 9c6b913..c207ef9 100644
--- a/src/core/thread/energy_scan_server.cpp
+++ b/src/core/thread/energy_scan_server.cpp
@@ -43,7 +43,6 @@
 #include "meshcop/meshcop.hpp"
 #include "meshcop/meshcop_tlvs.hpp"
 #include "thread/thread_netif.hpp"
-#include "thread/uri_paths.hpp"
 
 namespace ot {
 
@@ -56,26 +55,20 @@
     , mPeriod(0)
     , mScanDuration(0)
     , mCount(0)
-    , mActive(false)
-    , mScanResultsLength(0)
-    , mTimer(aInstance, EnergyScanServer::HandleTimer)
-    , mEnergyScan(UriPath::kEnergyScan, &EnergyScanServer::HandleRequest, this)
+    , mReportMessage(nullptr)
+    , mTimer(aInstance)
 {
-    Get<Tmf::Agent>().AddResource(mEnergyScan);
 }
 
-void EnergyScanServer::HandleRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
+template <>
+void EnergyScanServer::HandleTmf<kUriEnergyScan>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    static_cast<EnergyScanServer *>(aContext)->HandleRequest(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void EnergyScanServer::HandleRequest(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
-{
-    uint8_t          count;
-    uint16_t         period;
-    uint16_t         scanDuration;
-    Ip6::MessageInfo responseInfo(aMessageInfo);
-    uint32_t         mask;
+    uint8_t                 count;
+    uint16_t                period;
+    uint16_t                scanDuration;
+    uint32_t                mask;
+    MeshCoP::Tlv            tlv;
+    MeshCoP::ChannelMaskTlv channelMaskTlv;
 
     VerifyOrExit(aMessage.IsPostRequest());
 
@@ -85,40 +78,46 @@
 
     VerifyOrExit((mask = MeshCoP::ChannelMaskTlv::GetChannelMask(aMessage)) != 0);
 
+    FreeMessage(mReportMessage);
+    mReportMessage = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriEnergyReport);
+    VerifyOrExit(mReportMessage != nullptr);
+
+    channelMaskTlv.Init();
+    channelMaskTlv.SetChannelMask(mask);
+    SuccessOrExit(channelMaskTlv.AppendTo(*mReportMessage));
+
+    tlv.SetType(MeshCoP::Tlv::kEnergyList);
+    SuccessOrExit(mReportMessage->Append(tlv));
+
+    mNumScanResults     = 0;
     mChannelMask        = mask;
     mChannelMaskCurrent = mChannelMask;
     mCount              = count;
     mPeriod             = period;
     mScanDuration       = scanDuration;
-    mScanResultsLength  = 0;
-    mActive             = true;
     mTimer.Start(kScanDelay);
 
     mCommissioner = aMessageInfo.GetPeerAddr();
 
     if (aMessage.IsConfirmable() && !aMessageInfo.GetSockAddr().IsMulticast())
     {
-        SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, responseInfo));
-        LogInfo("sent energy scan query response");
+        SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
+        LogInfo("Sent %s ack", UriToString<kUriEnergyScan>());
     }
 
 exit:
     return;
 }
 
-void EnergyScanServer::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<EnergyScanServer>().HandleTimer();
-}
-
 void EnergyScanServer::HandleTimer(void)
 {
-    VerifyOrExit(mActive);
+    VerifyOrExit(mReportMessage != nullptr);
 
     if (mCount)
     {
         // grab the lowest channel to scan
         uint32_t channelMask = mChannelMaskCurrent & ~(mChannelMaskCurrent - 1);
+
         IgnoreError(Get<Mac::Mac>().EnergyScan(channelMask, mScanDuration, HandleScanResult, this));
     }
     else
@@ -137,16 +136,31 @@
 
 void EnergyScanServer::HandleScanResult(Mac::EnergyScanResult *aResult)
 {
-    VerifyOrExit(mActive);
+    VerifyOrExit(mReportMessage != nullptr);
 
     if (aResult)
     {
-        VerifyOrExit(mScanResultsLength < OPENTHREAD_CONFIG_TMF_ENERGY_SCAN_MAX_RESULTS);
-        mScanResults[mScanResultsLength++] = aResult->mMaxRssi;
+        if (mReportMessage->Append<int8_t>(aResult->mMaxRssi) != kErrorNone)
+        {
+            FreeMessage(mReportMessage);
+            mReportMessage = nullptr;
+            ExitNow();
+        }
+
+        mNumScanResults++;
+
+        if (mNumScanResults == NumericLimits<uint8_t>::kMax)
+        {
+            // If we reach the max length that fit in the Energy List
+            // TLV we send the current set of energy scan data.
+
+            mCount = 0;
+            mTimer.Start(kReportDelay);
+        }
     }
     else
     {
-        // clear the lowest channel to scan
+        // Clear the lowest channel to scan
         mChannelMaskCurrent &= mChannelMaskCurrent - 1;
 
         if (mChannelMaskCurrent == 0)
@@ -171,42 +185,33 @@
 
 void EnergyScanServer::SendReport(void)
 {
-    Error                   error = kErrorNone;
-    MeshCoP::ChannelMaskTlv channelMask;
-    MeshCoP::EnergyListTlv  energyList;
-    Tmf::MessageInfo        messageInfo(GetInstance());
-    Coap::Message *         message;
+    Error            error = kErrorNone;
+    Tmf::MessageInfo messageInfo(GetInstance());
+    uint16_t         offset;
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kEnergyReport);
-    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
-
-    channelMask.Init();
-    channelMask.SetChannelMask(mChannelMask);
-    SuccessOrExit(error = channelMask.AppendTo(*message));
-
-    energyList.Init();
-    energyList.SetLength(mScanResultsLength);
-    SuccessOrExit(error = message->Append(energyList));
-    SuccessOrExit(error = message->AppendBytes(mScanResults, mScanResultsLength));
+    // Update the Energy List TLV length in Report message
+    offset = mReportMessage->GetLength() - mNumScanResults - sizeof(uint8_t);
+    mReportMessage->Write(offset, mNumScanResults);
 
     messageInfo.SetSockAddrToRlocPeerAddrTo(mCommissioner);
 
-    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
+    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*mReportMessage, messageInfo));
 
-    LogInfo("sent scan results");
+    LogInfo("Sent %s", UriToString<kUriEnergyReport>());
 
 exit:
-    FreeMessageOnError(message, error);
+    FreeMessageOnError(mReportMessage, error);
     MeshCoP::LogError("send scan results", error);
-    mActive = false;
+    mReportMessage = nullptr;
 }
 
 void EnergyScanServer::HandleNotifierEvents(Events aEvents)
 {
-    if (aEvents.Contains(kEventThreadNetdataChanged) && !mActive &&
+    if (aEvents.Contains(kEventThreadNetdataChanged) && (mReportMessage != nullptr) &&
         Get<NetworkData::Leader>().GetCommissioningData() == nullptr)
     {
-        mActive = false;
+        mReportMessage->Free();
+        mReportMessage = nullptr;
         mTimer.Stop();
     }
 }
diff --git a/src/core/thread/energy_scan_server.hpp b/src/core/thread/energy_scan_server.hpp
index 1f168d1..ca7c7a5 100644
--- a/src/core/thread/energy_scan_server.hpp
+++ b/src/core/thread/energy_scan_server.hpp
@@ -36,7 +36,6 @@
 
 #include "openthread-core-config.h"
 
-#include "coap/coap.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "common/notifier.hpp"
@@ -44,6 +43,7 @@
 #include "net/ip6_address.hpp"
 #include "net/udp6.hpp"
 #include "thread/thread_tlvs.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -54,6 +54,7 @@
 class EnergyScanServer : public InstanceLocator, private NonCopyable
 {
     friend class ot::Notifier;
+    friend class Tmf::Agent;
 
 public:
     /**
@@ -63,38 +64,35 @@
     explicit EnergyScanServer(Instance &aInstance);
 
 private:
-    static constexpr uint32_t kScanDelay   = 1000; ///< SCAN_DELAY (milliseconds)
-    static constexpr uint32_t kReportDelay = 500;  ///< Delay before sending a report (milliseconds)
+    static constexpr uint32_t kScanDelay   = 1000; // SCAN_DELAY (milliseconds)
+    static constexpr uint32_t kReportDelay = 500;  // Delay before sending a report (milliseconds)
 
-    static void HandleRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleRequest(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
     static void HandleScanResult(Mac::EnergyScanResult *aResult, void *aContext);
     void        HandleScanResult(Mac::EnergyScanResult *aResult);
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTimer(void);
 
     void HandleNotifierEvents(Events aEvents);
 
     void SendReport(void);
 
-    Ip6::Address mCommissioner;
-    uint32_t     mChannelMask;
-    uint32_t     mChannelMaskCurrent;
-    uint16_t     mPeriod;
-    uint16_t     mScanDuration;
-    uint8_t      mCount;
-    bool         mActive;
+    using ScanTimer = TimerMilliIn<EnergyScanServer, &EnergyScanServer::HandleTimer>;
 
-    int8_t  mScanResults[OPENTHREAD_CONFIG_TMF_ENERGY_SCAN_MAX_RESULTS];
-    uint8_t mScanResultsLength;
-
-    TimerMilli mTimer;
-
-    Coap::Resource mEnergyScan;
+    Ip6::Address   mCommissioner;
+    uint32_t       mChannelMask;
+    uint32_t       mChannelMaskCurrent;
+    uint16_t       mPeriod;
+    uint16_t       mScanDuration;
+    uint8_t        mCount;
+    uint8_t        mNumScanResults;
+    Coap::Message *mReportMessage;
+    ScanTimer      mTimer;
 };
 
+DeclareTmfHandler(EnergyScanServer, kUriEnergyScan);
+
 /**
  * @}
  */
diff --git a/src/core/thread/indirect_sender.cpp b/src/core/thread/indirect_sender.cpp
index 6b4e68c..697ce35 100644
--- a/src/core/thread/indirect_sender.cpp
+++ b/src/core/thread/indirect_sender.cpp
@@ -345,13 +345,12 @@
         break;
 
     case Message::kTypeSupervision:
-        PrepareEmptyFrame(aFrame, aChild, kSupervisionMsgAckRequest);
+        PrepareEmptyFrame(aFrame, aChild, /* aAckRequest */ true);
         aContext.mMessageNextOffset = message->GetLength();
         break;
 
     default:
         OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
     }
 
 exit:
@@ -360,24 +359,24 @@
 
 uint16_t IndirectSender::PrepareDataFrame(Mac::TxFrame &aFrame, Child &aChild, Message &aMessage)
 {
-    Ip6::Header  ip6Header;
-    Mac::Address macSource, macDest;
-    uint16_t     directTxOffset;
-    uint16_t     nextOffset;
+    Ip6::Header    ip6Header;
+    Mac::Addresses macAddrs;
+    uint16_t       directTxOffset;
+    uint16_t       nextOffset;
 
     // Determine the MAC source and destination addresses.
 
     IgnoreError(aMessage.Read(0, ip6Header));
 
-    Get<MeshForwarder>().GetMacSourceAddress(ip6Header.GetSource(), macSource);
+    Get<MeshForwarder>().GetMacSourceAddress(ip6Header.GetSource(), macAddrs.mSource);
 
     if (ip6Header.GetDestination().IsLinkLocal())
     {
-        Get<MeshForwarder>().GetMacDestinationAddress(ip6Header.GetDestination(), macDest);
+        Get<MeshForwarder>().GetMacDestinationAddress(ip6Header.GetDestination(), macAddrs.mDestination);
     }
     else
     {
-        aChild.GetMacAddress(macDest);
+        aChild.GetMacAddress(macAddrs.mDestination);
     }
 
     // Prepare the data frame from previous child's indirect offset.
@@ -385,7 +384,7 @@
     directTxOffset = aMessage.GetOffset();
     aMessage.SetOffset(aChild.GetIndirectFragmentOffset());
 
-    nextOffset = Get<MeshForwarder>().PrepareDataFrame(aFrame, aMessage, macSource, macDest);
+    nextOffset = Get<MeshForwarder>().PrepareDataFrame(aFrame, aMessage, macAddrs);
 
     aMessage.SetOffset(directTxOffset);
 
@@ -412,19 +411,17 @@
 void IndirectSender::HandleSentFrameToChild(const Mac::TxFrame &aFrame,
                                             const FrameContext &aContext,
                                             Error               aError,
-                                            Child &             aChild)
+                                            Child              &aChild)
 {
     Message *message    = aChild.GetIndirectMessage();
     uint16_t nextOffset = aContext.mMessageNextOffset;
 
     VerifyOrExit(mEnabled);
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
     if (aError == kErrorNone)
     {
-        Get<Utils::ChildSupervisor>().UpdateOnSend(aChild);
+        Get<ChildSupervisor>().UpdateOnSend(aChild);
     }
-#endif
 
     // A zero `nextOffset` indicates that the sent frame is an empty
     // frame generated by `PrepareFrameForChild()` when there was no
@@ -465,7 +462,6 @@
 
     default:
         OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
     }
 
     if ((message != nullptr) && (nextOffset < message->GetLength()))
diff --git a/src/core/thread/indirect_sender.hpp b/src/core/thread/indirect_sender.hpp
index 4e5fdb9..f04b54f 100644
--- a/src/core/thread/indirect_sender.hpp
+++ b/src/core/thread/indirect_sender.hpp
@@ -203,12 +203,6 @@
     void HandleChildModeChange(Child &aChild, Mle::DeviceMode aOldMode);
 
 private:
-    /**
-     * Indicates whether to set/enable 15.4 ack request in the MAC header of a supervision message.
-     *
-     */
-    static constexpr bool kSupervisionMsgAckRequest = (OPENTHREAD_CONFIG_CHILD_SUPERVISION_MSG_NO_ACK_REQUEST == 0);
-
     // Callbacks from DataPollHandler
     Error PrepareFrameForChild(Mac::TxFrame &aFrame, FrameContext &aContext, Child &aChild);
     void  HandleSentFrameToChild(const Mac::TxFrame &aFrame, const FrameContext &aContext, Error aError, Child &aChild);
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 7563972..ddcbf74 100644
--- a/src/core/thread/key_manager.cpp
+++ b/src/core/thread/key_manager.cpp
@@ -172,7 +172,7 @@
     , mHoursSinceKeyRotation(0)
     , mKeySwitchGuardTime(kDefaultKeySwitchGuardTime)
     , mKeySwitchGuardEnabled(false)
-    , mKeyRotationTimer(aInstance, KeyManager::HandleKeyRotationTimer)
+    , mKeyRotationTimer(aInstance)
     , mKekFrameCounter(0)
     , mIsPskcSet(false)
 {
@@ -202,10 +202,7 @@
     StartKeyRotationTimer();
 }
 
-void KeyManager::Stop(void)
-{
-    mKeyRotationTimer.Stop();
-}
+void KeyManager::Stop(void) { mKeyRotationTimer.Stop(); }
 
 void KeyManager::SetPskc(const Pskc &aPskc)
 {
@@ -241,7 +238,7 @@
 
 #if OPENTHREAD_FTD
     // reset router frame counters
-    for (Router &router : Get<RouterTable>().Iterate())
+    for (Router &router : Get<RouterTable>())
     {
         router.SetKeySequence(0);
         router.GetLinkFrameCounters().Reset();
@@ -287,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)];
@@ -309,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)];
@@ -384,7 +381,7 @@
     mKeySequence = aKeySequence;
     UpdateKeyMaterial();
 
-    SetAllMacFrameCounters(0);
+    SetAllMacFrameCounters(0, /* aSetIfLarger */ false);
     mMleFrameCounter = 0;
 
     Get<Notifier>().Signal(kEventThreadKeySeqCounterChanged);
@@ -415,12 +412,14 @@
 }
 #endif
 
-void KeyManager::SetAllMacFrameCounters(uint32_t aMacFrameCounter)
+void KeyManager::SetAllMacFrameCounters(uint32_t aFrameCounter, bool aSetIfLarger)
 {
-    mMacFrameCounters.SetAll(aMacFrameCounter);
+    mMacFrameCounters.SetAll(aFrameCounter);
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
-    Get<Mac::SubMac>().SetFrameCounter(aMacFrameCounter);
+    Get<Mac::SubMac>().SetFrameCounter(aFrameCounter, aSetIfLarger);
+#else
+    OT_UNUSED_VARIABLE(aSetIfLarger);
 #endif
 }
 
@@ -444,9 +443,7 @@
     return;
 }
 #else
-void KeyManager::MacFrameCounterUsed(uint32_t)
-{
-}
+void KeyManager::MacFrameCounterUsed(uint32_t) {}
 #endif
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
@@ -497,11 +494,6 @@
     mKeyRotationTimer.Start(kOneHourIntervalInMsec);
 }
 
-void KeyManager::HandleKeyRotationTimer(Timer &aTimer)
-{
-    aTimer.Get<KeyManager>().HandleKeyRotationTimer();
-}
-
 void KeyManager::HandleKeyRotationTimer(void)
 {
     mHoursSinceKeyRotation++;
@@ -642,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 a8e2e32..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.
@@ -401,10 +401,12 @@
     /**
      * This method sets the current MAC Frame Counter value for all radio links.
      *
-     * @param[in]  aMacFrameCounter  The MAC Frame Counter value.
-     *
+     * @param[in] aFrameCounter  The MAC Frame Counter value.
+     * @param[in] aSetIfLarger   If `true`, set only if the new value @p aFrameCounter is larger than current value.
+     *                           If `false`, set the new value independent of the current value.
+
      */
-    void SetAllMacFrameCounters(uint32_t aMacFrameCounter);
+    void SetAllMacFrameCounters(uint32_t aFrameCounter, bool aSetIfLarger);
 
     /**
      * This method sets the MAC Frame Counter value which is stored in non-volatile memory.
@@ -445,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`.
      *
@@ -543,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;
@@ -569,15 +585,14 @@
         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);
-    static void HandleKeyRotationTimer(Timer &aTimer);
-    void        HandleKeyRotationTimer(void);
+    void StartKeyRotationTimer(void);
+    void HandleKeyRotationTimer(void);
 
 #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
     void StoreNetworkKey(const NetworkKey &aNetworkKey, bool aOverWriteExisting);
@@ -586,6 +601,8 @@
 
     void ResetFrameCounters(void);
 
+    using RotationTimer = TimerMilliIn<KeyManager, &KeyManager::HandleKeyRotationTimer>;
+
     static const uint8_t kThreadString[];
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
@@ -613,10 +630,10 @@
     uint32_t               mStoredMacFrameCounter;
     uint32_t               mStoredMleFrameCounter;
 
-    uint32_t   mHoursSinceKeyRotation;
-    uint32_t   mKeySwitchGuardTime;
-    bool       mKeySwitchGuardEnabled;
-    TimerMilli mKeyRotationTimer;
+    uint32_t      mHoursSinceKeyRotation;
+    uint32_t      mKeySwitchGuardTime;
+    bool          mKeySwitchGuardEnabled;
+    RotationTimer mKeyRotationTimer;
 
 #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
     PskcRef mPskcRef;
diff --git a/src/core/thread/link_metrics.cpp b/src/core/thread/link_metrics.cpp
index c9c917b..cfe784c 100644
--- a/src/core/thread/link_metrics.cpp
+++ b/src/core/thread/link_metrics.cpp
@@ -40,6 +40,8 @@
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
+#include "common/num_utils.hpp"
+#include "common/numeric_limits.hpp"
 #include "mac/mac.hpp"
 #include "thread/link_metrics_tlvs.hpp"
 #include "thread/neighbor_table.hpp"
@@ -49,160 +51,239 @@
 
 RegisterLogModule("LinkMetrics");
 
-using ot::Encoding::BigEndian::HostSwap32;
+static constexpr uint8_t kQueryIdSingleProbe = 0;   // This query ID represents Single Probe.
+static constexpr uint8_t kSeriesIdAllSeries  = 255; // This series ID represents all series.
 
-void SeriesInfo::Init(uint8_t aSeriesId, const SeriesFlags &aSeriesFlags, const Metrics &aMetrics)
-{
-    mSeriesId    = aSeriesId;
-    mSeriesFlags = aSeriesFlags;
-    mMetrics     = aMetrics;
-    mRssAverager.Clear();
-    mLqiAverager.Clear();
-    mPduCount = 0;
-}
+// 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;
 
-void SeriesInfo::AggregateLinkMetrics(uint8_t aFrameType, uint8_t aLqi, int8_t aRss)
-{
-    if (IsFrameTypeMatch(aFrameType))
-    {
-        mPduCount++;
-        mLqiAverager.Add(aLqi);
-        IgnoreError(mRssAverager.Add(aRss));
-    }
-}
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
 
-bool SeriesInfo::IsFrameTypeMatch(uint8_t aFrameType) const
-{
-    bool match = false;
-
-    switch (aFrameType)
-    {
-    case kSeriesTypeLinkProbe:
-        VerifyOrExit(!mSeriesFlags.IsMacDataFlagSet()); // Ignore this when Mac Data is accounted
-        match = mSeriesFlags.IsLinkProbeFlagSet();
-        break;
-    case Mac::Frame::kFcfFrameData:
-        match = mSeriesFlags.IsMacDataFlagSet();
-        break;
-    case Mac::Frame::kFcfFrameMacCmd:
-        match = mSeriesFlags.IsMacDataRequestFlagSet();
-        break;
-    case Mac::Frame::kFcfFrameAck:
-        match = mSeriesFlags.IsMacAckFlagSet();
-        break;
-    default:
-        break;
-    }
-
-exit:
-    return match;
-}
-
-LinkMetrics::LinkMetrics(Instance &aInstance)
+Initiator::Initiator(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mReportCallback(nullptr)
-    , mReportCallbackContext(nullptr)
-    , mMgmtResponseCallback(nullptr)
-    , mMgmtResponseCallbackContext(nullptr)
-    , mEnhAckProbingIeReportCallback(nullptr)
-    , mEnhAckProbingIeReportCallbackContext(nullptr)
 {
 }
 
-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)
 {
-    Error       error;
-    TypeIdFlags typeIdFlags[kMaxTypeIdFlags];
-    uint8_t     typeIdFlagsCount = 0;
-    Neighbor *  neighbor         = GetNeighborFromLinkLocalAddr(aDestination);
+    Error     error;
+    Neighbor *neighbor;
+    QueryInfo info;
 
-    VerifyOrExit(neighbor != nullptr, error = kErrorUnknownNeighbor);
-    VerifyOrExit(neighbor->IsThreadVersion1p2OrHigher(), error = kErrorNotCapable);
+    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));
+
+    info.Clear();
+    info.mSeriesId = aSeriesId;
 
     if (aMetrics != nullptr)
     {
-        typeIdFlagsCount = TypeIdFlagsFromMetrics(typeIdFlags, *aMetrics);
+        info.mTypeIdCount = aMetrics->ConvertToTypeIds(info.mTypeIds);
     }
 
     if (aSeriesId != 0)
     {
-        VerifyOrExit(typeIdFlagsCount == 0, error = kErrorInvalidArgs);
+        VerifyOrExit(info.mTypeIdCount == 0, error = kErrorInvalidArgs);
     }
 
-    error = SendLinkMetricsQuery(aDestination, aSeriesId, typeIdFlags, typeIdFlagsCount);
+    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::Info &aSeriesFlags,
-                                                        const Metrics *          aMetrics)
+Error Initiator::AppendLinkMetricsQueryTlv(Message &aMessage, const QueryInfo &aInfo)
 {
-    Error        error = kErrorNone;
-    uint8_t      subTlvs[sizeof(Tlv) + sizeof(uint8_t) * 2 + sizeof(TypeIdFlags) * kMaxTypeIdFlags];
-    Tlv *        fwdProbingSubTlv  = reinterpret_cast<Tlv *>(subTlvs);
-    SeriesFlags *seriesFlags       = reinterpret_cast<SeriesFlags *>(subTlvs + sizeof(Tlv) + sizeof(aSeriesId));
-    uint8_t      typeIdFlagsOffset = sizeof(Tlv) + sizeof(uint8_t) * 2;
-    uint8_t      typeIdFlagsCount  = 0;
-    Neighbor *   neighbor          = GetNeighborFromLinkLocalAddr(aDestination);
+    Error error = kErrorNone;
+    Tlv   tlv;
 
-    VerifyOrExit(neighbor != nullptr, error = kErrorUnknownNeighbor);
-    VerifyOrExit(neighbor->IsThreadVersion1p2OrHigher(), error = kErrorNotCapable);
+    // 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.
 
-    // Directly transform `aMetrics` into TypeIdFlags and put them into `subTlvs`
-    if (aMetrics != nullptr)
+    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)
     {
-        typeIdFlagsCount =
-            TypeIdFlagsFromMetrics(reinterpret_cast<TypeIdFlags *>(subTlvs + typeIdFlagsOffset), *aMetrics);
+        QueryOptionsSubTlv queryOptionsTlv;
+
+        queryOptionsTlv.Init();
+        queryOptionsTlv.SetLength(aInfo.mTypeIdCount);
+        SuccessOrExit(error = aMessage.Append(queryOptionsTlv));
+        SuccessOrExit(error = aMessage.AppendBytes(aInfo.mTypeIds, aInfo.mTypeIdCount));
     }
 
+exit:
+    return error;
+}
+
+void Initiator::HandleReport(const Message &aMessage, uint16_t aOffset, uint16_t aLength, const Ip6::Address &aAddress)
+{
+    Error         error     = kErrorNone;
+    uint16_t      offset    = aOffset;
+    uint16_t      endOffset = aOffset + aLength;
+    bool          hasStatus = false;
+    bool          hasReport = false;
+    Tlv           tlv;
+    ReportSubTlv  reportTlv;
+    MetricsValues values;
+    uint8_t       status;
+    uint8_t       typeId;
+
+    OT_UNUSED_VARIABLE(error);
+
+    VerifyOrExit(mReportCallback.IsSet());
+
+    values.Clear();
+
+    while (offset < endOffset)
+    {
+        SuccessOrExit(error = aMessage.Read(offset, tlv));
+
+        VerifyOrExit(offset + sizeof(Tlv) + tlv.GetLength() <= endOffset, error = kErrorParse);
+
+        // The report must contain either:
+        // - One or more Report Sub-TLVs (in case of success), or
+        // - A single Status Sub-TLV (in case of failure).
+
+        switch (tlv.GetType())
+        {
+        case StatusSubTlv::kType:
+            VerifyOrExit(!hasStatus && !hasReport, error = kErrorDrop);
+            SuccessOrExit(error = Tlv::Read<StatusSubTlv>(aMessage, offset, status));
+            hasStatus = true;
+            break;
+
+        case ReportSubTlv::kType:
+            VerifyOrExit(!hasStatus, error = kErrorDrop);
+
+            // Read the report sub-TLV assuming minimum length
+            SuccessOrExit(error = aMessage.Read(offset, &reportTlv, sizeof(Tlv) + ReportSubTlv::kMinLength));
+            VerifyOrExit(reportTlv.IsValid(), error = kErrorParse);
+            hasReport = true;
+
+            typeId = reportTlv.GetMetricsTypeId();
+
+            if (TypeId::IsExtended(typeId))
+            {
+                // Skip the sub-TLV if `E` flag is set.
+                break;
+            }
+
+            if (TypeId::GetValueLength(typeId) > sizeof(uint8_t))
+            {
+                // If Type ID indicates metric value has 4 bytes length, we
+                // read the full `reportTlv`.
+                SuccessOrExit(error = aMessage.Read(offset, reportTlv));
+            }
+
+            switch (typeId)
+            {
+            case TypeId::kPdu:
+                values.mMetrics.mPduCount = true;
+                values.mPduCountValue     = reportTlv.GetMetricsValue32();
+                LogDebg(" - PDU Counter: %lu (Count/Summation)", ToUlong(values.mPduCountValue));
+                break;
+
+            case TypeId::kLqi:
+                values.mMetrics.mLqi = true;
+                values.mLqiValue     = reportTlv.GetMetricsValue8();
+                LogDebg(" - LQI: %u (Exponential Moving Average)", values.mLqiValue);
+                break;
+
+            case TypeId::kLinkMargin:
+                values.mMetrics.mLinkMargin = true;
+                values.mLinkMarginValue     = ScaleRawValueToLinkMargin(reportTlv.GetMetricsValue8());
+                LogDebg(" - Margin: %u (dB) (Exponential Moving Average)", values.mLinkMarginValue);
+                break;
+
+            case TypeId::kRssi:
+                values.mMetrics.mRssi = true;
+                values.mRssiValue     = ScaleRawValueToRssi(reportTlv.GetMetricsValue8());
+                LogDebg(" - RSSI: %u (dBm) (Exponential Moving Average)", values.mRssiValue);
+                break;
+            }
+
+            break;
+        }
+
+        offset += sizeof(Tlv) + tlv.GetLength();
+    }
+
+    VerifyOrExit(hasStatus || hasReport);
+
+    mReportCallback.Invoke(&aAddress, hasStatus ? nullptr : &values,
+                           hasStatus ? static_cast<Status>(status) : kStatusSuccess);
+
+exit:
+    LogDebg("HandleReport, error:%s", ErrorToString(error));
+}
+
+Error Initiator::SendMgmtRequestForwardTrackingSeries(const Ip6::Address &aDestination,
+                                                      uint8_t             aSeriesId,
+                                                      const SeriesFlags  &aSeriesFlags,
+                                                      const Metrics      *aMetrics)
+{
+    Error               error;
+    Neighbor           *neighbor;
+    uint8_t             typeIdCount = 0;
+    FwdProbingRegSubTlv fwdProbingSubTlv;
+
+    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));
+
     VerifyOrExit(aSeriesId > kQueryIdSingleProbe, error = kErrorInvalidArgs);
 
-    fwdProbingSubTlv->SetType(SubTlv::kFwdProbingReg);
+    fwdProbingSubTlv.Init();
+    fwdProbingSubTlv.SetSeriesId(aSeriesId);
+    fwdProbingSubTlv.SetSeriesFlagsMask(aSeriesFlags.ConvertToMask());
 
-    // SeriesId + SeriesFlags + typeIdFlagsCount * TypeIdFlags
-    fwdProbingSubTlv->SetLength(sizeof(uint8_t) * 2 + sizeof(TypeIdFlags) * typeIdFlagsCount);
+    if (aMetrics != nullptr)
+    {
+        typeIdCount = aMetrics->ConvertToTypeIds(fwdProbingSubTlv.GetTypeIds());
+    }
 
-    memcpy(subTlvs + sizeof(Tlv), &aSeriesId, sizeof(aSeriesId));
+    fwdProbingSubTlv.SetLength(sizeof(aSeriesId) + sizeof(uint8_t) + typeIdCount);
 
-    seriesFlags->SetFrom(aSeriesFlags);
-
-    error = Get<Mle::MleRouter>().SendLinkMetricsManagementRequest(aDestination, subTlvs, fwdProbingSubTlv->GetSize());
+    error = Get<Mle::Mle>().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 Initiator::SendMgmtRequestEnhAckProbing(const Ip6::Address &aDestination,
+                                              EnhAckFlags         aEnhAckFlags,
+                                              const Metrics      *aMetrics)
 {
-    Error              error = kErrorNone;
+    Error              error;
+    Neighbor          *neighbor;
+    uint8_t            typeIdCount = 0;
     EnhAckConfigSubTlv enhAckConfigSubTlv;
-    Mac::Address       macAddress;
-    Neighbor *         neighbor = GetNeighborFromLinkLocalAddr(aDestination);
 
-    VerifyOrExit(neighbor != nullptr, error = kErrorUnknownNeighbor);
-    VerifyOrExit(neighbor->IsThreadVersion1p2OrHigher(), error = kErrorNotCapable);
+    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));
 
     if (aEnhAckFlags == kEnhAckClear)
     {
         VerifyOrExit(aMetrics == nullptr, error = kErrorInvalidArgs);
     }
 
+    enhAckConfigSubTlv.Init();
     enhAckConfigSubTlv.SetEnhAckFlags(aEnhAckFlags);
 
     if (aMetrics != nullptr)
     {
-        enhAckConfigSubTlv.SetTypeIdFlags(*aMetrics);
+        typeIdCount = aMetrics->ConvertToTypeIds(enhAckConfigSubTlv.GetTypeIds());
     }
 
-    error = Get<Mle::MleRouter>().SendLinkMetricsManagementRequest(
-        aDestination, reinterpret_cast<const uint8_t *>(&enhAckConfigSubTlv), enhAckConfigSubTlv.GetSize());
+    enhAckConfigSubTlv.SetLength(EnhAckConfigSubTlv::kMinLength + typeIdCount);
+
+    error = Get<Mle::Mle>().SendLinkMetricsManagementRequest(aDestination, enhAckConfigSubTlv);
 
     if (aMetrics != nullptr)
     {
@@ -220,45 +301,142 @@
     return error;
 }
 
-Error LinkMetrics::SendLinkProbe(const Ip6::Address &aDestination, uint8_t aSeriesId, uint8_t aLength)
+Error Initiator::HandleManagementResponse(const Message &aMessage, const Ip6::Address &aAddress)
 {
-    Error     error = kErrorNone;
+    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 = GetNeighborFromLinkLocalAddr(aDestination);
+    Neighbor *neighbor;
 
-    VerifyOrExit(neighbor != nullptr, error = kErrorUnknownNeighbor);
-    VerifyOrExit(neighbor->IsThreadVersion1p2OrHigher(), error = kErrorNotCapable);
+    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));
 
-    VerifyOrExit(aLength <= LinkMetrics::kLinkProbeMaxLen && aSeriesId != kQueryIdSingleProbe &&
-                     aSeriesId != kSeriesIdAllSeries,
+    VerifyOrExit(aLength <= kLinkProbeMaxLen && aSeriesId != kQueryIdSingleProbe && aSeriesId != kSeriesIdAllSeries,
                  error = kErrorInvalidArgs);
 
-    error = Get<Mle::MleRouter>().SendLinkProbe(aDestination, aSeriesId, buf, aLength);
+    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;
+
+    VerifyOrExit(mEnhAckProbingIeReportCallback.IsSet());
+
+    values.SetMetrics(aNeighbor.GetEnhAckProbingMetrics());
+
+    if (values.GetMetrics().mLqi && idx < aLength)
+    {
+        values.mLqiValue = aData[idx++];
+    }
+    if (values.GetMetrics().mLinkMargin && idx < aLength)
+    {
+        values.mLinkMarginValue = ScaleRawValueToLinkMargin(aData[idx++]);
+    }
+    if (values.GetMetrics().mRssi && idx < aLength)
+    {
+        values.mRssiValue = ScaleRawValueToRssi(aData[idx++]);
+    }
+
+    mEnhAckProbingIeReportCallback.Invoke(aNeighbor.GetRloc16(), &aNeighbor.GetExtAddress(), &values);
+
+exit:
+    return;
+}
+
+Error Initiator::FindNeighbor(const Ip6::Address &aDestination, Neighbor *&aNeighbor)
+{
+    Error        error = kErrorUnknownNeighbor;
+    Mac::Address macAddress;
+
+    aNeighbor = nullptr;
+
+    VerifyOrExit(aDestination.IsLinkLocal());
+    aDestination.GetIid().ConvertToMacAddress(macAddress);
+
+    aNeighbor = Get<NeighborTable>().FindNeighbor(macAddress);
+    VerifyOrExit(aNeighbor != nullptr);
+
+    VerifyOrExit(aNeighbor->GetVersion() >= kThreadVersion1p2, error = kErrorNotCapable);
+    error = kErrorNone;
+
+exit:
+    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)
+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;
-    uint8_t       length      = 0;
-    uint16_t      startOffset = aMessage.GetLength();
+    bool          hasQueryId = false;
+    uint16_t      length;
     uint16_t      offset;
     uint16_t      endOffset;
     MetricsValues values;
 
     values.Clear();
 
-    SuccessOrExit(error = Tlv::FindTlvValueOffset(aRequestMessage, Mle::Tlv::Type::kLinkMetricsQuery, offset,
-                                                  endOffset)); // `endOffset` is used to store tlv length here
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Parse MLE Link Metrics Query TLV and its sub-TLVs from
+    // `aRequestMessage`.
 
-    endOffset = offset + endOffset;
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aRequestMessage, Mle::Tlv::Type::kLinkMetricsQuery, offset, length));
+
+    endOffset = offset + length;
 
     while (offset < endOffset)
     {
@@ -272,36 +450,35 @@
             break;
 
         case SubTlv::kQueryOptions:
-            SuccessOrExit(error = ReadTypeIdFlagsFromMessage(aRequestMessage, offset + sizeof(tlv),
-                                                             static_cast<uint16_t>(offset + tlv.GetSize()),
-                                                             values.GetMetrics()));
+            SuccessOrExit(error = ReadTypeIdsFromMessage(aRequestMessage, offset + sizeof(tlv),
+                                                         static_cast<uint16_t>(offset + tlv.GetSize()),
+                                                         values.GetMetrics()));
             break;
 
         default:
             break;
         }
 
-        offset += tlv.GetSize();
+        offset += static_cast<uint16_t>(tlv.GetSize());
     }
 
     VerifyOrExit(hasQueryId, error = kErrorParse);
 
-    // Link Metrics Report TLV
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // 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 = HostSwap32(aRequestMessage.GetPsduCount());
-        values.mLqiValue      = aRequestMessage.GetAverageLqi();
-        // Linearly scale Link Margin from [0, 130] to [0, 255]
-        values.mLinkMarginValue =
-            LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(), aRequestMessage.GetAverageRss()) *
-            255 / 130;
-        // Linearly scale rss from [-130, 0] to [0, 255]
-        values.mRssiValue = (aRequestMessage.GetAverageRss() + 130) * 255 / 130;
-
-        SuccessOrExit(error = AppendReportSubTlvToMessage(aMessage, length, values));
+        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
     {
@@ -309,259 +486,106 @@
 
         if (seriesInfo == nullptr)
         {
-            SuccessOrExit(error = AppendStatusSubTlvToMessage(aMessage, length, kStatusSeriesIdNotRecognized));
+            SuccessOrExit(error = Tlv::Append<StatusSubTlv>(aMessage, kStatusSeriesIdNotRecognized));
         }
         else if (seriesInfo->GetPduCount() == 0)
         {
-            SuccessOrExit(error = AppendStatusSubTlvToMessage(aMessage, length, kStatusNoMatchingFramesReceived));
+            SuccessOrExit(error = Tlv::Append<StatusSubTlv>(aMessage, kStatusNoMatchingFramesReceived));
         }
         else
         {
             values.SetMetrics(seriesInfo->GetLinkMetrics());
-            values.mPduCountValue = HostSwap32(seriesInfo->GetPduCount());
-            values.mLqiValue      = seriesInfo->GetAverageLqi();
-            // Linearly scale Link Margin from [0, 130] to [0, 255]
-            values.mLinkMarginValue =
-                LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(), seriesInfo->GetAverageRss()) *
-                255 / 130;
-            // Linearly scale RSSI from [-130, 0] to [0, 255]
-            values.mRssiValue = (seriesInfo->GetAverageRss() + 130) * 255 / 130;
-            SuccessOrExit(error = AppendReportSubTlvToMessage(aMessage, length, values));
+            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));
         }
     }
 
-    tlv.SetLength(length);
-    aMessage.Write(startOffset, tlv);
+    // 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 Subject::HandleManagementRequest(const Message &aMessage, Neighbor &aNeighbor, Status &aStatus)
 {
-    Error       error = kErrorNone;
-    Tlv         tlv;
-    uint8_t     seriesId;
-    SeriesFlags seriesFlags;
-    EnhAckFlags enhAckFlags;
-    Metrics     metrics;
-    bool        hasForwardProbingRegistrationTlv = false;
-    bool        hasEnhAckProbingTlv              = false;
-    uint16_t    offset;
-    uint16_t    length;
-    uint16_t    index = 0;
+    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;
 
-    while (index < 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)
     {
-        uint16_t pos = offset + index;
+        Tlv      tlv;
+        uint16_t minTlvSize;
+        Tlv     *subTlv;
 
-        SuccessOrExit(aMessage.Read(pos, tlv));
+        SuccessOrExit(error = aMessage.Read(offset, tlv));
 
-        pos += sizeof(tlv);
+        VerifyOrExit(offset + tlv.GetSize() <= endOffset, error = kErrorParse);
+        tlvEndOffset = static_cast<uint16_t>(offset + tlv.GetSize());
 
         switch (tlv.GetType())
         {
         case SubTlv::kFwdProbingReg:
-            VerifyOrExit(!hasForwardProbingRegistrationTlv && !hasEnhAckProbingTlv, error = kErrorParse);
-            VerifyOrExit(tlv.GetLength() >= sizeof(seriesId) + sizeof(seriesFlags), error = kErrorParse);
-            SuccessOrExit(aMessage.Read(pos, seriesId));
-            pos += sizeof(seriesId);
-            SuccessOrExit(aMessage.Read(pos, seriesFlags));
-            pos += sizeof(seriesFlags);
-            SuccessOrExit(error = ReadTypeIdFlagsFromMessage(
-                              aMessage, pos, static_cast<uint16_t>(offset + index + tlv.GetSize()), metrics));
-            hasForwardProbingRegistrationTlv = true;
+            subTlv     = &fwdProbingSubTlv;
+            minTlvSize = sizeof(Tlv) + FwdProbingRegSubTlv::kMinLength;
             break;
 
         case SubTlv::kEnhAckConfig:
-            VerifyOrExit(!hasForwardProbingRegistrationTlv && !hasEnhAckProbingTlv, error = kErrorParse);
-            VerifyOrExit(tlv.GetLength() >= sizeof(EnhAckFlags), error = kErrorParse);
-            SuccessOrExit(aMessage.Read(pos, enhAckFlags));
-            pos += sizeof(enhAckFlags);
-            SuccessOrExit(error = ReadTypeIdFlagsFromMessage(
-                              aMessage, pos, static_cast<uint16_t>(offset + index + tlv.GetSize()), metrics));
-            hasEnhAckProbingTlv = true;
+            subTlv     = &enhAckConfigSubTlv;
+            minTlvSize = sizeof(Tlv) + EnhAckConfigSubTlv::kMinLength;
             break;
 
         default:
-            break;
+            continue;
         }
 
-        index += tlv.GetSize();
+        // 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 (hasForwardProbingRegistrationTlv)
+    if (fwdProbingSubTlv.GetLength() != 0)
     {
-        aStatus = ConfigureForwardTrackingSeries(seriesId, seriesFlags, metrics, aNeighbor);
+        aStatus = ConfigureForwardTrackingSeries(fwdProbingSubTlv.GetSeriesId(), fwdProbingSubTlv.GetSeriesFlagsMask(),
+                                                 metrics, aNeighbor);
     }
-    else if (hasEnhAckProbingTlv)
+
+    if (enhAckConfigSubTlv.GetLength() != 0)
     {
-        aStatus = ConfigureEnhAckProbing(enhAckFlags, metrics, aNeighbor);
+        aStatus = ConfigureEnhAckProbing(enhAckConfigSubTlv.GetEnhAckFlags(), metrics, aNeighbor);
     }
 
 exit:
     return error;
 }
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
 
-Error LinkMetrics::HandleManagementResponse(const Message &aMessage, const Ip6::Address &aAddress)
-{
-    Error    error = kErrorNone;
-    Tlv      tlv;
-    uint16_t offset;
-    uint16_t length;
-    uint16_t index = 0;
-    Status   status;
-    bool     hasStatus = false;
-
-    VerifyOrExit(mMgmtResponseCallback != nullptr);
-
-    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Mle::Tlv::Type::kLinkMetricsManagement, offset, length));
-
-    while (index < length)
-    {
-        SuccessOrExit(aMessage.Read(offset + index, tlv));
-
-        switch (tlv.GetType())
-        {
-        case SubTlv::kStatus:
-            VerifyOrExit(!hasStatus, error = kErrorParse);
-            VerifyOrExit(tlv.GetLength() == sizeof(status), error = kErrorParse);
-            SuccessOrExit(aMessage.Read(offset + index + sizeof(tlv), status));
-            hasStatus = true;
-            break;
-
-        default:
-            break;
-        }
-
-        index += tlv.GetSize();
-    }
-
-    VerifyOrExit(hasStatus, error = kErrorParse);
-
-    mMgmtResponseCallback(&aAddress, status, mMgmtResponseCallbackContext);
-
-exit:
-    return error;
-}
-
-void LinkMetrics::HandleReport(const Message &     aMessage,
-                               uint16_t            aOffset,
-                               uint16_t            aLength,
-                               const Ip6::Address &aAddress)
-{
-    Error         error = kErrorNone;
-    MetricsValues values;
-    uint8_t       rawValue;
-    uint16_t      pos    = aOffset;
-    uint16_t      endPos = aOffset + aLength;
-    Tlv           tlv;
-    TypeIdFlags   typeIdFlags;
-    bool          hasStatus = false;
-    bool          hasReport = false;
-    Status        status;
-
-    OT_UNUSED_VARIABLE(error);
-
-    VerifyOrExit(mReportCallback != nullptr);
-
-    values.Clear();
-
-    while (pos < endPos)
-    {
-        SuccessOrExit(aMessage.Read(pos, tlv));
-        VerifyOrExit(tlv.GetType() == SubTlv::kReport);
-        pos += sizeof(Tlv);
-
-        VerifyOrExit(pos + tlv.GetLength() <= endPos, error = kErrorParse);
-
-        switch (tlv.GetType())
-        {
-        case SubTlv::kStatus:
-            // There should be either: one Status TLV or some Report-Sub TLVs
-            VerifyOrExit(!hasStatus && !hasReport, error = kErrorDrop);
-            VerifyOrExit(tlv.GetLength() == sizeof(status), error = kErrorParse);
-            SuccessOrExit(aMessage.Read(pos, status));
-            hasStatus = true;
-            pos += sizeof(status);
-            break;
-
-        case SubTlv::kReport:
-            // There shouldn't be any Report-Sub TLV when there's a Status TLV
-            VerifyOrExit(!hasStatus, error = kErrorDrop);
-            VerifyOrExit(tlv.GetLength() > sizeof(typeIdFlags), error = kErrorParse);
-            SuccessOrExit(aMessage.Read(pos, typeIdFlags));
-
-            if (typeIdFlags.IsExtendedFlagSet())
-            {
-                pos += tlv.GetLength(); // Skip the whole sub-TLV if `E` flag is set
-                continue;
-            }
-
-            hasReport = true;
-            pos += sizeof(TypeIdFlags);
-
-            switch (typeIdFlags.GetRawValue())
-            {
-            case TypeIdFlags::kPdu:
-                values.GetMetrics().mPduCount = true;
-                SuccessOrExit(aMessage.Read(pos, values.mPduCountValue));
-                values.mPduCountValue = HostSwap32(values.mPduCountValue);
-                pos += sizeof(uint32_t);
-                LogDebg(" - PDU Counter: %d (Count/Summation)", values.mPduCountValue);
-                break;
-
-            case TypeIdFlags::kLqi:
-                values.GetMetrics().mLqi = true;
-                SuccessOrExit(aMessage.Read(pos, values.mLqiValue));
-                pos += sizeof(uint8_t);
-                LogDebg(" - LQI: %d (Exponential Moving Average)", values.mLqiValue);
-                break;
-
-            case TypeIdFlags::kLinkMargin:
-                values.GetMetrics().mLinkMargin = true;
-                SuccessOrExit(aMessage.Read(pos, rawValue));
-                // Reverse operation for linear scale, map from [0, 255] to [0, 130]
-                values.mLinkMarginValue = rawValue * 130 / 255;
-                pos += sizeof(uint8_t);
-                LogDebg(" - Margin: %d (dB) (Exponential Moving Average)", values.mLinkMarginValue);
-                break;
-
-            case TypeIdFlags::kRssi:
-                values.GetMetrics().mRssi = true;
-                SuccessOrExit(aMessage.Read(pos, rawValue));
-                // Reverse operation for linear scale, map from [0, 255] to [-130, 0]
-                values.mRssiValue = rawValue * 130 / 255 - 130;
-                pos += sizeof(uint8_t);
-                LogDebg(" - RSSI: %d (dBm) (Exponential Moving Average)", values.mRssiValue);
-                break;
-
-            default:
-                break;
-            }
-            break;
-        }
-    }
-
-    if (hasStatus)
-    {
-        mReportCallback(&aAddress, nullptr, status, mReportCallbackContext);
-    }
-    else if (hasReport)
-    {
-        mReportCallback(&aAddress, &values, OT_LINK_METRICS_STATUS_SUCCESS, mReportCallbackContext);
-    }
-
-exit:
-    LogDebg("HandleReport, error:%s", ErrorToString(error));
-    return;
-}
-
-Error LinkMetrics::HandleLinkProbe(const Message &aMessage, uint8_t &aSeriesId)
+Error Subject::HandleLinkProbe(const Message &aMessage, uint8_t &aSeriesId)
 {
     Error    error = kErrorNone;
     uint16_t offset;
@@ -575,114 +599,111 @@
     return error;
 }
 
-void LinkMetrics::SetReportCallback(ReportCallback aCallback, void *aContext)
+Error Subject::AppendReportSubTlvToMessage(Message &aMessage, const MetricsValues &aValues)
 {
-    mReportCallback        = aCallback;
-    mReportCallbackContext = aContext;
-}
+    Error        error = kErrorNone;
+    ReportSubTlv reportTlv;
 
-void LinkMetrics::SetMgmtResponseCallback(MgmtResponseCallback aCallback, void *aContext)
-{
-    mMgmtResponseCallback        = aCallback;
-    mMgmtResponseCallbackContext = aContext;
-}
+    reportTlv.Init();
 
-void LinkMetrics::SetEnhAckProbingCallback(EnhAckProbingIeReportCallback aCallback, void *aContext)
-{
-    mEnhAckProbingIeReportCallback        = aCallback;
-    mEnhAckProbingIeReportCallbackContext = aContext;
-}
-
-void LinkMetrics::ProcessEnhAckIeData(const uint8_t *aData, uint8_t aLength, const Neighbor &aNeighbor)
-{
-    MetricsValues values;
-    uint8_t       idx = 0;
-
-    VerifyOrExit(mEnhAckProbingIeReportCallback != nullptr);
-
-    values.SetMetrics(aNeighbor.GetEnhAckProbingMetrics());
-
-    if (values.GetMetrics().mLqi && idx < aLength)
+    if (aValues.mMetrics.mPduCount)
     {
-        values.mLqiValue = aData[idx++];
-    }
-    if (values.GetMetrics().mLinkMargin && idx < aLength)
-    {
-        // Reverse operation for linear scale, map from [0, 255] to [0, 130]
-        values.mLinkMarginValue = aData[idx++] * 130 / 255;
-    }
-    if (values.GetMetrics().mRssi && idx < aLength)
-    {
-        // Reverse operation for linear scale, map from [0, 255] to [-130, 0]
-        values.mRssiValue = aData[idx++] * 130 / 255 - 130;
+        reportTlv.SetMetricsTypeId(TypeId::kPdu);
+        reportTlv.SetMetricsValue32(aValues.mPduCountValue);
+        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
     }
 
-    mEnhAckProbingIeReportCallback(aNeighbor.GetRloc16(), &aNeighbor.GetExtAddress(), &values,
-                                   mEnhAckProbingIeReportCallbackContext);
-
-exit:
-    return;
-}
-
-Error LinkMetrics::SendLinkMetricsQuery(const Ip6::Address &aDestination,
-                                        uint8_t             aSeriesId,
-                                        const TypeIdFlags * aTypeIdFlags,
-                                        uint8_t             aTypeIdFlagsCount)
-{
-    // LinkMetricsQuery Tlv + LinkMetricsQueryId sub-TLV (value-length: 1 byte) +
-    // LinkMetricsQueryOptions sub-TLV (value-length: `kMaxTypeIdFlags` bytes)
-    constexpr uint16_t kBufferSize = sizeof(Tlv) * 3 + sizeof(uint8_t) + sizeof(TypeIdFlags) * kMaxTypeIdFlags;
-
-    Error                error = kErrorNone;
-    QueryOptionsSubTlv   queryOptionsTlv;
-    uint8_t              length = 0;
-    static const uint8_t tlvs[] = {Mle::Tlv::kLinkMetricsReport};
-    uint8_t              buf[kBufferSize];
-    Tlv *                tlv = reinterpret_cast<Tlv *>(buf);
-    Tlv                  subTlv;
-
-    // Link Metrics Query TLV
-    tlv->SetType(Mle::Tlv::kLinkMetricsQuery);
-    length += sizeof(Tlv);
-
-    // Link Metrics Query ID sub-TLV
-    subTlv.SetType(SubTlv::kQueryId);
-    subTlv.SetLength(sizeof(uint8_t));
-    memcpy(buf + length, &subTlv, sizeof(subTlv));
-    length += sizeof(subTlv);
-    memcpy(buf + length, &aSeriesId, sizeof(aSeriesId));
-    length += sizeof(aSeriesId);
-
-    // Link Metrics Query Options sub-TLV
-    if (aTypeIdFlagsCount > 0)
+    if (aValues.mMetrics.mLqi)
     {
-        queryOptionsTlv.Init();
-        queryOptionsTlv.SetLength(aTypeIdFlagsCount * sizeof(TypeIdFlags));
-
-        memcpy(buf + length, &queryOptionsTlv, sizeof(queryOptionsTlv));
-        length += sizeof(queryOptionsTlv);
-        memcpy(buf + length, aTypeIdFlags, queryOptionsTlv.GetLength());
-        length += queryOptionsTlv.GetLength();
+        reportTlv.SetMetricsTypeId(TypeId::kLqi);
+        reportTlv.SetMetricsValue8(aValues.mLqiValue);
+        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
     }
 
-    // Set Length for Link Metrics Report TLV
-    tlv->SetLength(length - sizeof(Tlv));
+    if (aValues.mMetrics.mLinkMargin)
+    {
+        reportTlv.SetMetricsTypeId(TypeId::kLinkMargin);
+        reportTlv.SetMetricsValue8(ScaleLinkMarginToRawValue(aValues.mLinkMarginValue));
+        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
+    }
 
-    SuccessOrExit(error = Get<Mle::MleRouter>().SendDataRequest(aDestination, tlvs, sizeof(tlvs), 0, buf, length));
+    if (aValues.mMetrics.mRssi)
+    {
+        reportTlv.SetMetricsTypeId(TypeId::kRssi);
+        reportTlv.SetMetricsValue8(ScaleRssiToRawValue(aValues.mRssiValue));
+        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
+    }
 
 exit:
     return error;
 }
 
-Status LinkMetrics::ConfigureForwardTrackingSeries(uint8_t            aSeriesId,
-                                                   const SeriesFlags &aSeriesFlags,
-                                                   const Metrics &    aMetrics,
-                                                   Neighbor &         aNeighbor)
+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;
+
+    aMetrics.Clear();
+
+    for (uint16_t offset = aStartOffset; offset < aEndOffset; offset++)
+    {
+        uint8_t typeId;
+
+        SuccessOrExit(aMessage.Read(offset, typeId));
+
+        switch (typeId)
+        {
+        case TypeId::kPdu:
+            VerifyOrExit(!aMetrics.mPduCount, error = kErrorParse);
+            aMetrics.mPduCount = true;
+            break;
+
+        case TypeId::kLqi:
+            VerifyOrExit(!aMetrics.mLqi, error = kErrorParse);
+            aMetrics.mLqi = true;
+            break;
+
+        case TypeId::kLinkMargin:
+            VerifyOrExit(!aMetrics.mLinkMargin, error = kErrorParse);
+            aMetrics.mLinkMargin = true;
+            break;
+
+        case TypeId::kRssi:
+            VerifyOrExit(!aMetrics.mRssi, error = kErrorParse);
+            aMetrics.mRssi = true;
+            break;
+
+        default:
+            if (TypeId::IsExtended(typeId))
+            {
+                offset += sizeof(uint8_t); // Skip the additional second byte.
+            }
+            else
+            {
+                aMetrics.mReserved = true;
+            }
+            break;
+        }
+    }
+
+exit:
+    return error;
+}
+
+Status Subject::ConfigureForwardTrackingSeries(uint8_t        aSeriesId,
+                                               uint8_t        aSeriesFlagsMask,
+                                               const Metrics &aMetrics,
+                                               Neighbor      &aNeighbor)
 {
     Status status = kStatusSuccess;
 
     VerifyOrExit(0 < aSeriesId, status = kStatusOtherError);
-    if (aSeriesFlags.GetRawValue() == 0) // Remove the series
+
+    if (aSeriesFlagsMask == 0) // Remove the series
     {
         if (aSeriesId == kSeriesIdAllSeries) // Remove all
         {
@@ -702,7 +723,7 @@
         seriesInfo = mSeriesInfoPool.Allocate();
         VerifyOrExit(seriesInfo != nullptr, status = kStatusCannotSupportNewSeries);
 
-        seriesInfo->Init(aSeriesId, aSeriesFlags, aMetrics);
+        seriesInfo->Init(aSeriesId, aSeriesFlagsMask, aMetrics);
 
         aNeighbor.AddForwardTrackingSeriesInfo(*seriesInfo);
     }
@@ -711,8 +732,7 @@
     return status;
 }
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-Status LinkMetrics::ConfigureEnhAckProbing(EnhAckFlags aEnhAckFlags, const Metrics &aMetrics, Neighbor &aNeighbor)
+Status Subject::ConfigureEnhAckProbing(uint8_t aEnhAckFlags, const Metrics &aMetrics, Neighbor &aNeighbor)
 {
     Status status = kStatusSuccess;
     Error  error  = kErrorNone;
@@ -742,136 +762,57 @@
 exit:
     return status;
 }
+
 #endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
 
-Neighbor *LinkMetrics::GetNeighborFromLinkLocalAddr(const Ip6::Address &aDestination)
+uint8_t ScaleLinkMarginToRawValue(uint8_t aLinkMargin)
 {
-    Neighbor *   neighbor = nullptr;
-    Mac::Address macAddress;
+    // Linearly scale Link Margin from [0, 130] to [0, 255].
+    // `kMaxLinkMargin = 130`.
 
-    VerifyOrExit(aDestination.IsLinkLocal());
-    aDestination.GetIid().ConvertToMacAddress(macAddress);
-    neighbor = Get<NeighborTable>().FindNeighbor(macAddress);
+    uint16_t value;
 
-exit:
-    return neighbor;
+    value = Min(aLinkMargin, kMaxLinkMargin);
+    value = value * NumericLimits<uint8_t>::kMax;
+    value = DivideAndRoundToClosest<uint16_t>(value, kMaxLinkMargin);
+
+    return static_cast<uint8_t>(value);
 }
 
-Error LinkMetrics::ReadTypeIdFlagsFromMessage(const Message &aMessage,
-                                              uint8_t        aStartPos,
-                                              uint8_t        aEndPos,
-                                              Metrics &      aMetrics)
+uint8_t ScaleRawValueToLinkMargin(uint8_t aRawValue)
 {
-    Error error = kErrorNone;
+    // Scale back raw value of [0, 255] to Link Margin from [0, 130].
 
-    memset(&aMetrics, 0, sizeof(aMetrics));
+    uint16_t value = aRawValue;
 
-    for (uint16_t pos = aStartPos; pos < aEndPos; pos += sizeof(TypeIdFlags))
-    {
-        TypeIdFlags typeIdFlags;
-
-        SuccessOrExit(aMessage.Read(pos, typeIdFlags));
-
-        switch (typeIdFlags.GetRawValue())
-        {
-        case TypeIdFlags::kPdu:
-            VerifyOrExit(!aMetrics.mPduCount, error = kErrorParse);
-            aMetrics.mPduCount = true;
-            break;
-
-        case TypeIdFlags::kLqi:
-            VerifyOrExit(!aMetrics.mLqi, error = kErrorParse);
-            aMetrics.mLqi = true;
-            break;
-
-        case TypeIdFlags::kLinkMargin:
-            VerifyOrExit(!aMetrics.mLinkMargin, error = kErrorParse);
-            aMetrics.mLinkMargin = true;
-            break;
-
-        case TypeIdFlags::kRssi:
-            VerifyOrExit(!aMetrics.mRssi, error = kErrorParse);
-            aMetrics.mRssi = true;
-            break;
-
-        default:
-            if (typeIdFlags.IsExtendedFlagSet())
-            {
-                pos += sizeof(uint8_t); // Skip the additional second flags byte.
-            }
-            else
-            {
-                aMetrics.mReserved = true;
-            }
-            break;
-        }
-    }
-
-exit:
-    return error;
+    value = value * kMaxLinkMargin;
+    value = DivideAndRoundToClosest<uint16_t>(value, NumericLimits<uint8_t>::kMax);
+    return static_cast<uint8_t>(value);
 }
 
-Error LinkMetrics::AppendReportSubTlvToMessage(Message &aMessage, uint8_t &aLength, const MetricsValues &aValues)
+uint8_t ScaleRssiToRawValue(int8_t aRssi)
 {
-    Error        error = kErrorNone;
-    ReportSubTlv metric;
+    // Linearly scale RSSI from [-130, 0] to [0, 255].
+    // `kMinRssi = -130`, `kMaxRssi = 0`.
 
-    aLength = 0;
+    int32_t value = aRssi;
 
-    // Link Metrics Report sub-TLVs
-    if (aValues.mMetrics.mPduCount)
-    {
-        metric.Init();
-        metric.SetMetricsTypeId(TypeIdFlags(TypeIdFlags::kPdu));
-        metric.SetMetricsValue32(aValues.mPduCountValue);
-        SuccessOrExit(error = aMessage.AppendBytes(&metric, metric.GetSize()));
-        aLength += metric.GetSize();
-    }
+    value = Clamp(value, kMinRssi, kMaxRssi) - kMinRssi;
+    value = value * NumericLimits<uint8_t>::kMax;
+    value = DivideAndRoundToClosest<int32_t>(value, kMaxRssi - kMinRssi);
 
-    if (aValues.mMetrics.mLqi)
-    {
-        metric.Init();
-        metric.SetMetricsTypeId(TypeIdFlags(TypeIdFlags::kLqi));
-        metric.SetMetricsValue8(aValues.mLqiValue);
-        SuccessOrExit(error = aMessage.AppendBytes(&metric, metric.GetSize()));
-        aLength += metric.GetSize();
-    }
-
-    if (aValues.mMetrics.mLinkMargin)
-    {
-        metric.Init();
-        metric.SetMetricsTypeId(TypeIdFlags(TypeIdFlags::kLinkMargin));
-        metric.SetMetricsValue8(aValues.mLinkMarginValue);
-        SuccessOrExit(error = aMessage.AppendBytes(&metric, metric.GetSize()));
-        aLength += metric.GetSize();
-    }
-
-    if (aValues.mMetrics.mRssi)
-    {
-        metric.Init();
-        metric.SetMetricsTypeId(TypeIdFlags(TypeIdFlags::kRssi));
-        metric.SetMetricsValue8(aValues.mRssiValue);
-        SuccessOrExit(error = aMessage.AppendBytes(&metric, metric.GetSize()));
-        aLength += metric.GetSize();
-    }
-
-exit:
-    return error;
+    return static_cast<uint8_t>(value);
 }
 
-Error LinkMetrics::AppendStatusSubTlvToMessage(Message &aMessage, uint8_t &aLength, Status aStatus)
+int8_t ScaleRawValueToRssi(uint8_t aRawValue)
 {
-    Error error = kErrorNone;
-    Tlv   statusTlv;
+    int32_t value = aRawValue;
 
-    statusTlv.SetType(SubTlv::kStatus);
-    statusTlv.SetLength(sizeof(uint8_t));
-    SuccessOrExit(error = aMessage.AppendBytes(&statusTlv, sizeof(statusTlv)));
-    SuccessOrExit(error = aMessage.AppendBytes(&aStatus, sizeof(aStatus)));
-    aLength += statusTlv.GetSize();
+    value = value * (kMaxRssi - kMinRssi);
+    value = DivideAndRoundToClosest<int32_t>(value, NumericLimits<uint8_t>::kMax);
+    value += kMinRssi;
 
-exit:
-    return error;
+    return static_cast<int8_t>(value);
 }
 
 } // namespace LinkMetrics
diff --git a/src/core/thread/link_metrics.hpp b/src/core/thread/link_metrics.hpp
index 318aea0..0dcdd99 100644
--- a/src/core/thread/link_metrics.hpp
+++ b/src/core/thread/link_metrics.hpp
@@ -46,6 +46,7 @@
 #include <openthread/link.h>
 
 #include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/clearable.hpp"
 #include "common/locator.hpp"
 #include "common/message.hpp"
@@ -57,6 +58,7 @@
 
 namespace ot {
 class Neighbor;
+class UnitTester;
 
 namespace LinkMetrics {
 
@@ -69,172 +71,40 @@
  * @{
  */
 
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+
 /**
- * This type represents the results (values) for a set of metrics.
+ * This class implements the Thread Link Metrics Initiator.
  *
- * @sa otLinkMetricsValues.
+ * The Initiator makes queries, configures Link Metrics probing at the Subject and generates reports of the results.
  *
  */
-class MetricsValues : public otLinkMetricsValues, public Clearable<MetricsValues>
+class Initiator : public InstanceLocator, private NonCopyable
 {
 public:
-    /**
-     * This method gets the metrics flags.
-     *
-     * @returns The metrics flags.
-     *
-     */
-    Metrics &GetMetrics(void) { return static_cast<Metrics &>(mMetrics); }
-
-    /**
-     * This method gets the metrics flags.
-     *
-     * @returns The metrics flags.
-     *
-     */
-    const Metrics &GetMetrics(void) const { return static_cast<const Metrics &>(mMetrics); }
-
-    /**
-     * This method set the metrics flags.
-     *
-     * @param[in] aMetrics  The metrics flags to set from.
-     *
-     */
-    void SetMetrics(const Metrics &aMetrics) { mMetrics = aMetrics; }
-};
-
-/**
- * This class represents one Series that is being tracked by the Subject.
- *
- * When an Initiator successfully configured a Forward Tracking Series, the Subject would use an instance of this class
- * to track the information of the Series. The Subject has a `Pool` of `SeriesInfo`. It would allocate one when a new
- * Series comes, and free it when a Series finishes.
- *
- * This class inherits `LinkedListEntry` and each `Neighbor` has a list of `SeriesInfo` so that the Subject could track
- * per Series initiated by neighbors as long as it has available resources.
- *
- */
-class SeriesInfo : public LinkedListEntry<SeriesInfo>
-{
-    friend class LinkedList<SeriesInfo>;
-    friend class LinkedListEntry<SeriesInfo>;
-
-public:
-    /**
-     * This constant represents Link Probe when filtering frames to be accounted using Series Flag. There's
-     * already `kFcfFrameData`, `kFcfFrameAck` and `kFcfFrameMacCmd`. This item is added so that we can
-     * filter a Link Probe for series in the same way as other frames.
-     *
-     */
-    static constexpr uint8_t kSeriesTypeLinkProbe = 0;
-
-    /**
-     * This method initializes the SeriesInfo object.
-     *
-     * @param[in]  aSeriesId      The Series ID.
-     * @param[in]  aSeriesFlags   The Series Flags which specify what types of frames are to be accounted.
-     * @param[in]  aMetrics       Metrics to query.
-     *
-     */
-    void Init(uint8_t aSeriesId, const SeriesFlags &aSeriesFlags, const Metrics &aMetrics);
-
-    /**
-     * This method gets the Series ID.
-     *
-     * @returns  The Series ID.
-     *
-     */
-    uint8_t GetSeriesId(void) const { return mSeriesId; }
-
-    /**
-     * This method gets the PDU count.
-     *
-     * @returns  The PDU count.
-     *
-     */
-    uint32_t GetPduCount(void) const { return mPduCount; }
-
-    /**
-     * This method gets the average LQI.
-     *
-     * @returns  The average LQI.
-     *
-     */
-    uint8_t GetAverageLqi(void) const { return mLqiAverager.GetAverage(); }
-
-    /**
-     * This method gets the average RSS.
-     *
-     * @returns  The average RSS.
-     *
-     */
-    int8_t GetAverageRss(void) const { return mRssAverager.GetAverage(); }
-
-    /**
-     * This method aggregates the Link Metrics data of a frame into this series.
-     *
-     * @param[in]  aFrameType    The type of the frame.
-     * @param[in]  aLqi          The LQI value.
-     * @param[in]  aRss          The RSS value.
-     *
-     */
-    void AggregateLinkMetrics(uint8_t aFrameType, uint8_t aLqi, int8_t aRss);
-
-    /**
-     * This methods gets the metrics.
-     *
-     * @returns  The metrics associated with `SeriesInfo`.
-     *
-     */
-    const Metrics &GetLinkMetrics(void) const { return mMetrics; }
-
-private:
-    bool Matches(const uint8_t &aSeriesId) const { return mSeriesId == aSeriesId; }
-    bool IsFrameTypeMatch(uint8_t aFrameType) const;
-
-    SeriesInfo *mNext;
-    uint8_t     mSeriesId;
-    SeriesFlags mSeriesFlags;
-    Metrics     mMetrics;
-    RssAverager mRssAverager;
-    LqiAverager mLqiAverager;
-    uint32_t    mPduCount;
-};
-
-/**
- * This enumeration type represent Link Metrics Status.
- *
- */
-enum Status : uint8_t
-{
-    kStatusSuccess                   = OT_LINK_METRICS_STATUS_SUCCESS,
-    kStatusCannotSupportNewSeries    = OT_LINK_METRICS_STATUS_CANNOT_SUPPORT_NEW_SERIES,
-    kStatusSeriesIdAlreadyRegistered = OT_LINK_METRICS_STATUS_SERIESID_ALREADY_REGISTERED,
-    kStatusSeriesIdNotRecognized     = OT_LINK_METRICS_STATUS_SERIESID_NOT_RECOGNIZED,
-    kStatusNoMatchingFramesReceived  = OT_LINK_METRICS_STATUS_NO_MATCHING_FRAMES_RECEIVED,
-    kStatusOtherError                = OT_LINK_METRICS_STATUS_OTHER_ERROR,
-};
-
-/**
- * This class implements Thread Link Metrics query and management.
- *
- */
-class LinkMetrics : public InstanceLocator, private NonCopyable
-{
-    friend class ot::Neighbor;
-
-public:
+    // Initiator callbacks
     typedef otLinkMetricsReportCallback                ReportCallback;
     typedef otLinkMetricsMgmtResponseCallback          MgmtResponseCallback;
     typedef otLinkMetricsEnhAckProbingIeReportCallback EnhAckProbingIeReportCallback;
 
     /**
-     * This constructor initializes an instance of the LinkMetrics class.
+     * This structure provides the info used for appending MLE Link Metric Query TLV.
+     *
+     */
+    struct QueryInfo : public Clearable<QueryInfo>
+    {
+        uint8_t mSeriesId;             ///< Series ID.
+        uint8_t mTypeIds[kMaxTypeIds]; ///< Type IDs.
+        uint8_t mTypeIdCount;          ///< Number of entries in `mTypeIds[]`.
+    };
+
+    /**
+     * 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.
@@ -247,13 +117,45 @@
      *
      * @retval kErrorNone             Successfully sent a Link Metrics query message.
      * @retval kErrorNoBufs           Insufficient buffers to generate the MLE Data Request message.
-     * @retval kErrorInvalidArgs      TypeIdFlags are not valid or exceed the count limit.
+     * @retval kErrorInvalidArgs      Type IDs are not valid or exceed the count limit.
      * @retval kErrorUnknownNeighbor  @p aDestination is not link-local or the neighbor is not found.
      *
      */
     Error Query(const Ip6::Address &aDestination, uint8_t aSeriesId, const Metrics *aMetrics);
 
     /**
+     * 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.
      *
      * @param[in] aDestination       A reference to the IPv6 address of the destination.
@@ -267,12 +169,23 @@
      * @retval kErrorUnknownNeighbor  @p aDestination is not link-local or the neighbor is not found.
      *
      */
-    Error SendMgmtRequestForwardTrackingSeries(const Ip6::Address &     aDestination,
-                                               uint8_t                  aSeriesId,
-                                               const SeriesFlags::Info &aSeriesFlags,
-                                               const Metrics *          aMetrics);
+    Error SendMgmtRequestForwardTrackingSeries(const Ip6::Address &aDestination,
+                                               uint8_t             aSeriesId,
+                                               const SeriesFlags  &aSeriesFlags,
+                                               const Metrics      *aMetrics);
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    /**
+     * 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.
      *
@@ -290,7 +203,31 @@
      */
     Error SendMgmtRequestEnhAckProbing(const Ip6::Address &aDestination,
                                        EnhAckFlags         aEnhAckFlags,
-                                       const Metrics *     aMetrics);
+                                       const Metrics      *aMetrics);
+
+    /**
+     * 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 handles the received Link Metrics Management Response contained in @p aMessage.
+     *
+     * @param[in]  aMessage    A reference to the message that contains the Link Metrics Management Response.
+     * @param[in]  aAddress    A reference to the source address of the message.
+     *
+     * @retval kErrorNone     Successfully handled the Link Metrics Management Response.
+     * @retval kErrorParse    Cannot parse sub-TLVs from @p aMessage successfully.
+     *
+     */
+    Error HandleManagementResponse(const Message &aMessage, const Ip6::Address &aAddress);
 
     /**
      * This method sends an MLE Link Probe message.
@@ -306,9 +243,50 @@
      *
      */
     Error SendLinkProbe(const Ip6::Address &aDestination, uint8_t aSeriesId, uint8_t aLength);
-#endif
+
+    /**
+     * 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.
      *
@@ -322,7 +300,7 @@
      *
      */
     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.
      *
@@ -337,29 +315,6 @@
     Error HandleManagementRequest(const Message &aMessage, Neighbor &aNeighbor, Status &aStatus);
 
     /**
-     * This method handles the received Link Metrics Management Response contained in @p aMessage.
-     *
-     * @param[in]  aMessage    A reference to the message that contains the Link Metrics Management Response.
-     * @param[in]  aAddress    A reference to the source address of the message.
-     *
-     * @retval kErrorNone     Successfully handled the Link Metrics Management Response.
-     * @retval kErrorParse    Cannot parse sub-TLVs from @p aMessage successfully.
-     *
-     */
-    Error HandleManagementResponse(const Message &aMessage, const Ip6::Address &aAddress);
-
-    /**
-     * 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 handles the Link Probe contained in @p aMessage.
      *
      * @param[in]   aMessage     A reference to the message that contains the Link Probe Message.
@@ -372,93 +327,44 @@
     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);
-
-    /**
-     * 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);
-
-    /**
-     * 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);
-
-    /**
-     * 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);
+    void Free(SeriesInfo &aSeriesInfo);
 
 private:
-    static constexpr uint8_t kMaxTypeIdFlags = 4;
-
     // 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.
+    static Error ReadTypeIdsFromMessage(const Message &aMessage,
+                                        uint16_t       aStartOffset,
+                                        uint16_t       aEndOffset,
+                                        Metrics       &aMetrics);
+    static Error AppendReportSubTlvToMessage(Message &aMessage, const MetricsValues &aValues);
 
-    Error SendLinkMetricsQuery(const Ip6::Address &aDestination,
-                               uint8_t             aSeriesId,
-                               const TypeIdFlags * aTypeIdFlags,
-                               uint8_t             aTypeIdFlagsCount);
-
-    Status ConfigureForwardTrackingSeries(uint8_t            aSeriesId,
-                                          const SeriesFlags &aSeriesFlags,
-                                          const Metrics &    aMetrics,
-                                          Neighbor &         aNeighbor);
-
-    Status ConfigureEnhAckProbing(EnhAckFlags aEnhAckFlags, const Metrics &aMetrics, Neighbor &aNeighbor);
-
-    Neighbor *GetNeighborFromLinkLocalAddr(const Ip6::Address &aDestination);
-
-    static Error ReadTypeIdFlagsFromMessage(const Message &aMessage,
-                                            uint8_t        aStartPos,
-                                            uint8_t        aEndPos,
-                                            Metrics &      aMetrics);
-    static Error AppendReportSubTlvToMessage(Message &aMessage, uint8_t &aLength, const MetricsValues &aValues);
-    static Error AppendStatusSubTlvToMessage(Message &aMessage, uint8_t &aLength, Status aStatus);
-
-    ReportCallback                mReportCallback;
-    void *                        mReportCallbackContext;
-    MgmtResponseCallback          mMgmtResponseCallback;
-    void *                        mMgmtResponseCallbackContext;
-    EnhAckProbingIeReportCallback mEnhAckProbingIeReportCallback;
-    void *                        mEnhAckProbingIeReportCallbackContext;
+    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);
+
 /**
  * @}
  */
 
 } // namespace LinkMetrics
-
-DefineCoreType(otLinkMetrics, LinkMetrics::Metrics);
-DefineCoreType(otLinkMetricsValues, LinkMetrics::MetricsValues);
-DefineMapEnum(otLinkMetricsEnhAckFlags, LinkMetrics::EnhAckFlags);
-
 } // namespace ot
 
 #endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
diff --git a/src/core/thread/link_metrics_tlvs.hpp b/src/core/thread/link_metrics_tlvs.hpp
index 118f730..ccf4af1 100644
--- a/src/core/thread/link_metrics_tlvs.hpp
+++ b/src/core/thread/link_metrics_tlvs.hpp
@@ -45,19 +45,12 @@
 #include "common/encoding.hpp"
 #include "common/message.hpp"
 #include "common/tlvs.hpp"
+#include "thread/link_metrics_types.hpp"
 
 namespace ot {
 namespace LinkMetrics {
 
-/**
- * This type represents Link Metric Flags indicating a set of metrics.
- *
- * @sa otLinkMetrics
- *
- */
-class Metrics : public otLinkMetrics, public Clearable<Metrics>
-{
-};
+using ot::Encoding::BigEndian::HostSwap32;
 
 /**
  * This class defines constants related to Link Metrics Sub-TLVs.
@@ -88,208 +81,10 @@
 typedef UintTlvInfo<SubTlv::kQueryId, uint8_t> QueryIdSubTlv;
 
 /**
- * This class implements Link Metrics Type ID Flags generation and parsing.
+ * This type defines a Link Metrics Status Sub-Tlv.
  *
  */
-OT_TOOL_PACKED_BEGIN class TypeIdFlags
-{
-    static constexpr uint8_t kExtendedFlag     = 1 << 7;
-    static constexpr uint8_t kLengthOffset     = 6;
-    static constexpr uint8_t kLengthFlag       = 1 << kLengthOffset;
-    static constexpr uint8_t kTypeEnumOffset   = 3;
-    static constexpr uint8_t kTypeEnumMask     = 7 << kTypeEnumOffset;
-    static constexpr uint8_t kMetricEnumOffset = 0;
-    static constexpr uint8_t kMetricEnumMask   = 7 << kMetricEnumOffset;
-
-public:
-    /**
-     * This enumeration specifies the Length field in Type ID Flags.
-     *
-     */
-    enum Length
-    {
-        kShortLength    = 0, ///< Short value length (1 byte value)
-        kExtendedLength = 1, ///< Extended value length (4 bytes value)
-    };
-
-    /**
-     * This enumeration specifies the Type values in Type ID Flags.
-     *
-     */
-    enum TypeEnum : uint8_t
-    {
-        kTypeCountSummation   = 0, ///< Count or summation
-        kTypeExpMovingAverage = 1, ///< Exponential moving average.
-        kTypeReserved         = 2, ///< Reserved for future use.
-    };
-
-    /**
-     * This enumeration specifies the Metric values in Type ID Flag.
-     *
-     */
-    enum MetricEnum : uint8_t
-    {
-        kMetricPdusReceived = 0, ///< Number of PDUs received.
-        kMetricLqi          = 1, ///< Link Quality Indicator.
-        kMetricLinkMargin   = 2, ///< Link Margin.
-        kMetricRssi         = 3, ///< RSSI in dbm.
-    };
-
-    /**
-     * This constant defines the raw value for Type ID Flag for PDU.
-     *
-     */
-    static constexpr uint8_t kPdu = (kExtendedLength << kLengthOffset) | (kTypeCountSummation << kTypeEnumOffset) |
-                                    (kMetricPdusReceived << kMetricEnumOffset);
-
-    /**
-     * This constant defines the raw value for Type ID Flag for LQI.
-     *
-     */
-    static constexpr uint8_t kLqi = (kShortLength << kLengthOffset) | (kTypeExpMovingAverage << kTypeEnumOffset) |
-                                    (kMetricLqi << kMetricEnumOffset);
-
-    /**
-     * This constant defines the raw value for Type ID Flag for Link Margin.
-     *
-     */
-    static constexpr uint8_t kLinkMargin = (kShortLength << kLengthOffset) |
-                                           (kTypeExpMovingAverage << kTypeEnumOffset) |
-                                           (kMetricLinkMargin << kMetricEnumOffset);
-
-    /**
-     * This constant defines the raw value for Type ID Flag for RSSI
-     *
-     */
-    static constexpr uint8_t kRssi = (kShortLength << kLengthOffset) | (kTypeExpMovingAverage << kTypeEnumOffset) |
-                                     (kMetricRssi << kMetricEnumOffset);
-
-    /**
-     * Default constructor.
-     *
-     */
-    TypeIdFlags(void) = default;
-
-    /**
-     * Constructor to initialize from raw value.
-     *
-     * @param[in] aFlags  The raw flags value.
-     *
-     */
-    explicit TypeIdFlags(uint8_t aFlags)
-        : mFlags(aFlags)
-    {
-    }
-
-    /**
-     * This method initializes the Type ID value
-     *
-     */
-    void Init(void) { mFlags = 0; }
-
-    /**
-     * This method clears the Extended flag.
-     *
-     */
-    void ClearExtendedFlag(void) { mFlags &= ~kExtendedFlag; }
-
-    /**
-     * This method sets the Extended flag, indicating an additional second flags byte after the current 1-byte flags.
-     * MUST NOT set in Thread 1.2.1.
-     *
-     */
-    void SetExtendedFlag(void) { mFlags |= kExtendedFlag; }
-
-    /**
-     * This method indicates whether or not the Extended flag is set.
-     *
-     * @retval true   The Extended flag is set.
-     * @retval false  The Extended flag is not set.
-     *
-     */
-    bool IsExtendedFlagSet(void) const { return (mFlags & kExtendedFlag) != 0; }
-
-    /**
-     * This method clears value length flag.
-     *
-     */
-    void ClearLengthFlag(void) { mFlags &= ~kLengthFlag; }
-
-    /**
-     * This method sets the value length flag.
-     *
-     */
-    void SetLengthFlag(void) { mFlags |= kLengthFlag; }
-
-    /**
-     * This method indicates whether or not the value length flag is set.
-     *
-     * @retval true   The value length flag is set, extended value length (4 bytes)
-     * @retval false  The value length flag is not set, short value length (1 byte)
-     *
-     */
-    bool IsLengthFlagSet(void) const { return (mFlags & kLengthFlag) != 0; }
-
-    /**
-     * This method sets the Type/Average Enum.
-     *
-     * @param[in]  aTypeEnum  Type/Average Enum.
-     *
-     */
-    void SetTypeEnum(TypeEnum aTypeEnum)
-    {
-        mFlags = (mFlags & ~kTypeEnumMask) | ((aTypeEnum << kTypeEnumOffset) & kTypeEnumMask);
-    }
-
-    /**
-     * This method returns the Type/Average Enum.
-     *
-     * @returns The Type/Average Enum.
-     *
-     */
-    TypeEnum GetTypeEnum(void) const { return static_cast<TypeEnum>((mFlags & kTypeEnumMask) >> kTypeEnumOffset); }
-
-    /**
-     * This method sets the Metric Enum.
-     *
-     * @param[in]  aMetricEnum  Metric Enum.
-     *
-     */
-    void SetMetricEnum(MetricEnum aMetricEnum)
-    {
-        mFlags = (mFlags & ~kMetricEnumMask) | ((aMetricEnum << kMetricEnumOffset) & kMetricEnumMask);
-    }
-
-    /**
-     * This method returns the Metric Enum.
-     *
-     * @returns The Metric Enum.
-     *
-     */
-    MetricEnum GetMetricEnum(void) const
-    {
-        return static_cast<MetricEnum>((mFlags & kMetricEnumMask) >> kMetricEnumOffset);
-    }
-
-    /**
-     * This method returns the raw value of the entire TypeIdFlags.
-     *
-     * @returns The raw value of TypeIdFlags.
-     *
-     */
-    uint8_t GetRawValue(void) const { return mFlags; }
-
-    /**
-     * This method sets the raw value of the entire TypeIdFlags.
-     *
-     * @param[in]  aFlags  The raw flags value.
-     *
-     */
-    void SetRawValue(uint8_t aFlags) { mFlags = aFlags; }
-
-private:
-    uint8_t mFlags;
-} OT_TOOL_PACKED_END;
+typedef UintTlvInfo<SubTlv::kStatus, uint8_t> StatusSubTlv;
 
 /**
  * This class implements Link Metrics Report Sub-TLV generation and parsing.
@@ -299,15 +94,13 @@
 class ReportSubTlv : public Tlv, public TlvInfo<SubTlv::kReport>
 {
 public:
+    static constexpr uint8_t kMinLength = 2; ///< Minimum expected TLV length (type ID and u8 value).
+
     /**
      * This method initializes the TLV.
      *
      */
-    void Init(void)
-    {
-        SetType(SubTlv::kReport);
-        SetLength(sizeof(*this) - sizeof(Tlv));
-    }
+    void Init(void) { SetType(SubTlv::kReport); }
 
     /**
      * This method indicates whether or not the TLV appears to be well-formed.
@@ -316,7 +109,7 @@
      * @retval false  The TLV does not appear to be well-formed.
      *
      */
-    bool IsValid(void) const { return GetLength() >= sizeof(TypeIdFlags) + sizeof(uint8_t); }
+    bool IsValid(void) const { return GetLength() >= kMinLength; }
 
     /**
      * This method returns the Link Metrics Type ID.
@@ -324,7 +117,7 @@
      * @returns The Link Metrics Type ID.
      *
      */
-    TypeIdFlags GetMetricsTypeId(void) const { return mMetricsTypeId; }
+    uint8_t GetMetricsTypeId(void) const { return mMetricsTypeId; }
 
     /**
      * This method sets the Link Metrics Type ID.
@@ -332,15 +125,7 @@
      * @param[in]  aMetricsTypeId  The Link Metrics Type ID to set.
      *
      */
-    void SetMetricsTypeId(TypeIdFlags aMetricsTypeId)
-    {
-        mMetricsTypeId = aMetricsTypeId;
-
-        if (!aMetricsTypeId.IsLengthFlagSet())
-        {
-            SetLength(sizeof(*this) - sizeof(Tlv) - sizeof(uint32_t) + sizeof(uint8_t)); // The value is 1 byte long
-        }
-    }
+    void SetMetricsTypeId(uint8_t aMetricsTypeId) { mMetricsTypeId = aMetricsTypeId; }
 
     /**
      * This method returns the metric value in 8 bits.
@@ -356,7 +141,7 @@
      * @returns The metric value.
      *
      */
-    uint32_t GetMetricsValue32(void) const { return mMetricsValue.m32; }
+    uint32_t GetMetricsValue32(void) const { return HostSwap32(mMetricsValue.m32); }
 
     /**
      * This method sets the metric value (8 bits).
@@ -364,7 +149,11 @@
      * @param[in]  aMetricsValue  Metrics value.
      *
      */
-    void SetMetricsValue8(uint8_t aMetricsValue) { mMetricsValue.m8 = aMetricsValue; }
+    void SetMetricsValue8(uint8_t aMetricsValue)
+    {
+        mMetricsValue.m8 = aMetricsValue;
+        SetLength(kMinLength);
+    }
 
     /**
      * This method sets the metric value (32 bits).
@@ -372,10 +161,14 @@
      * @param[in]  aMetricsValue  Metrics value.
      *
      */
-    void SetMetricsValue32(uint32_t aMetricsValue) { mMetricsValue.m32 = aMetricsValue; }
+    void SetMetricsValue32(uint32_t aMetricsValue)
+    {
+        mMetricsValue.m32 = HostSwap32(aMetricsValue);
+        SetLength(sizeof(*this) - sizeof(Tlv));
+    }
 
 private:
-    TypeIdFlags mMetricsTypeId;
+    uint8_t mMetricsTypeId;
     union
     {
         uint8_t  m8;
@@ -408,227 +201,90 @@
      * @retval FALSE  If the TLV does not appear to be well-formed.
      *
      */
-    bool IsValid(void) const { return GetLength() >= sizeof(TypeIdFlags); }
+    bool IsValid(void) const { return GetLength() >= sizeof(uint8_t); }
 
 } OT_TOOL_PACKED_END;
 
 /**
- * This class implements Series Flags for Forward Tracking Series.
+ * This class defines Link Metrics Forward Probing Registration Sub-TLV.
  *
  */
 OT_TOOL_PACKED_BEGIN
-class SeriesFlags
+class FwdProbingRegSubTlv : public Tlv, public TlvInfo<SubTlv::kFwdProbingReg>
 {
 public:
-    /**
-     * This type represents which frames to be accounted in a Forward Tracking Series.
-     *
-     * @sa otLinkMetricsSeriesFlags
-     *
-     */
-    typedef otLinkMetricsSeriesFlags Info;
+    static constexpr uint8_t kMinLength = sizeof(uint8_t) + sizeof(uint8_t); ///< Minimum expected TLV length
 
     /**
-     * Default constructor.
+     * This method initializes the TLV.
      *
      */
-    SeriesFlags(void)
-        : mFlags(0)
+    void Init(void)
     {
+        SetType(SubTlv::kFwdProbingReg);
+        SetLength(kMinLength);
     }
 
     /**
-     * This method sets the values from an `Info` object.
+     * This method indicates whether or not the TLV appears to be well-formed.
      *
-     * @param[in]  aSeriesFlags  The `Info` object.
+     * @retval true   The TLV appears to be well-formed.
+     * @retval false  The TLV does not appear to be well-formed.
      *
      */
-    void SetFrom(const Info &aSeriesFlags)
-    {
-        Clear();
-
-        if (aSeriesFlags.mLinkProbe)
-        {
-            SetLinkProbeFlag();
-        }
-
-        if (aSeriesFlags.mMacData)
-        {
-            SetMacDataFlag();
-        }
-
-        if (aSeriesFlags.mMacDataRequest)
-        {
-            SetMacDataRequestFlag();
-        }
-
-        if (aSeriesFlags.mMacAck)
-        {
-            SetMacAckFlag();
-        }
-    }
+    bool IsValid(void) const { return GetLength() >= kMinLength; }
 
     /**
-     * This method clears the Link Probe flag.
+     * This method gets the Forward Series ID value.
+     *
+     * @returns The Forward Series ID.
      *
      */
-    void ClearLinkProbeFlag(void) { mFlags &= ~kLinkProbeFlag; }
+    uint8_t GetSeriesId(void) const { return mSeriesId; }
 
     /**
-     * This method sets the Link Probe flag.
+     * This method sets the Forward Series ID value.
+     *
+     * @param[in] aSeriesId  The Forward Series ID.
      *
      */
-    void SetLinkProbeFlag(void) { mFlags |= kLinkProbeFlag; }
+    void SetSeriesId(uint8_t aSeriesId) { mSeriesId = aSeriesId; }
 
     /**
-     * This method indicates whether or not the Link Probe flag is set.
+     * This method gets the Forward Series Flags bit-mask.
      *
-     * @retval true   The Link Probe flag is set.
-     * @retval false  The Link Probe flag is not set.
+     * @returns The Forward Series Flags mask.
      *
      */
-    bool IsLinkProbeFlagSet(void) const { return (mFlags & kLinkProbeFlag) != 0; }
+    uint8_t GetSeriesFlagsMask(void) const { return mSeriesFlagsMask; }
 
     /**
-     * This method clears the MAC Data flag.
+     * This method sets the Forward Series Flags bit-mask
+     *
+     * @param[in] aSeriesFlagsMask  The Forward Series Flags.
      *
      */
-    void ClearMacDataFlag(void) { mFlags &= ~kMacDataFlag; }
+    void SetSeriesFlagsMask(uint8_t aSeriesFlagsMask) { mSeriesFlagsMask = aSeriesFlagsMask; }
 
     /**
-     * This method sets the MAC Data flag.
+     * This method gets the start of Type ID array.
+     *
+     * @returns The start of Type ID array. Array has `kMaxTypeIds` max length.
      *
      */
-    void SetMacDataFlag(void) { mFlags |= kMacDataFlag; }
-
-    /**
-     * This method indicates whether or not the MAC Data flag is set.
-     *
-     * @retval true   The MAC Data flag is set.
-     * @retval false  The MAC Data flag is not set.
-     *
-     */
-    bool IsMacDataFlagSet(void) const { return (mFlags & kMacDataFlag) != 0; }
-
-    /**
-     * This method clears the MAC Data Request flag.
-     *
-     */
-    void ClearMacDataRequestFlag(void) { mFlags &= ~kMacDataRequestFlag; }
-
-    /**
-     * This method sets the MAC Data Request flag.
-     *
-     */
-    void SetMacDataRequestFlag(void) { mFlags |= kMacDataRequestFlag; }
-
-    /**
-     * This method indicates whether or not the MAC Data Request flag is set.
-     *
-     * @retval true   The MAC Data Request flag is set.
-     * @retval false  The MAC Data Request flag is not set.
-     *
-     */
-    bool IsMacDataRequestFlagSet(void) const { return (mFlags & kMacDataRequestFlag) != 0; }
-
-    /**
-     * This method clears the Mac Ack flag.
-     *
-     */
-    void ClearMacAckFlag(void) { mFlags &= ~kMacAckFlag; }
-
-    /**
-     * This method sets the Mac Ack flag.
-     *
-     */
-    void SetMacAckFlag(void) { mFlags |= kMacAckFlag; }
-
-    /**
-     * This method indicates whether or not the Mac Ack flag is set.
-     *
-     * @retval true   The Mac Ack flag is set.
-     * @retval false  The Mac Ack flag is not set.
-     *
-     */
-    bool IsMacAckFlagSet(void) const { return (mFlags & kMacAckFlag) != 0; }
-
-    /**
-     * This method returns the raw value of flags.
-     *
-     */
-    uint8_t GetRawValue(void) const { return mFlags; }
-
-    /**
-     * This method clears the all the flags.
-     *
-     */
-    void Clear(void) { mFlags = 0; }
+    uint8_t *GetTypeIds(void) { return mTypeIds; }
 
 private:
-    static constexpr uint8_t kLinkProbeFlag      = 1 << 0;
-    static constexpr uint8_t kMacDataFlag        = 1 << 1;
-    static constexpr uint8_t kMacDataRequestFlag = 1 << 2;
-    static constexpr uint8_t kMacAckFlag         = 1 << 3;
-
-    uint8_t mFlags;
+    uint8_t mSeriesId;
+    uint8_t mSeriesFlagsMask;
+    uint8_t mTypeIds[kMaxTypeIds];
 } OT_TOOL_PACKED_END;
 
-/**
- * This enumeration type represent Enhanced-ACK Flags.
- *
- */
-enum EnhAckFlags : uint8_t
-{
-    kEnhAckClear    = OT_LINK_METRICS_ENH_ACK_CLEAR,    ///< Clear.
-    kEnhAckRegister = OT_LINK_METRICS_ENH_ACK_REGISTER, ///< Register.
-};
-
-static uint8_t TypeIdFlagsFromMetrics(TypeIdFlags aTypeIdFlags[], const Metrics &aMetrics)
-{
-    uint8_t count = 0;
-
-    if (aMetrics.mPduCount)
-    {
-        aTypeIdFlags[count++].SetRawValue(TypeIdFlags::kPdu);
-    }
-
-    if (aMetrics.mLqi)
-    {
-        aTypeIdFlags[count++].SetRawValue(TypeIdFlags::kLqi);
-    }
-
-    if (aMetrics.mLinkMargin)
-    {
-        aTypeIdFlags[count++].SetRawValue(TypeIdFlags::kLinkMargin);
-    }
-
-    if (aMetrics.mRssi)
-    {
-        aTypeIdFlags[count++].SetRawValue(TypeIdFlags::kRssi);
-    }
-
-#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-    if (aMetrics.mReserved)
-    {
-        for (uint8_t i = 0; i < count; i++)
-        {
-            aTypeIdFlags[i].SetTypeEnum(TypeIdFlags::kTypeReserved);
-        }
-    }
-#endif
-
-    return count;
-}
-
 OT_TOOL_PACKED_BEGIN
 class EnhAckConfigSubTlv : public Tlv, public TlvInfo<SubTlv::kEnhAckConfig>
 {
 public:
-    /**
-     * Default constructor
-     *
-     */
-    EnhAckConfigSubTlv(void) { Init(); }
+    static constexpr uint8_t kMinLength = sizeof(uint8_t); ///< Minimum TLV length (only `EnhAckFlags`).
 
     /**
      * This method initializes the TLV.
@@ -637,43 +293,45 @@
     void Init(void)
     {
         SetType(SubTlv::kEnhAckConfig);
-        SetLength(sizeof(EnhAckFlags));
+        SetLength(kMinLength);
     }
 
     /**
+     * This method indicates whether or not the TLV appears to be well-formed.
+     *
+     * @retval true   The TLV appears to be well-formed.
+     * @retval false  The TLV does not appear to be well-formed.
+     *
+     */
+    bool IsValid(void) const { return GetLength() >= kMinLength; }
+
+    /**
+     * This method gets the Enhanced ACK Flags.
+     *
+     * @returns The Enhanced ACK Flags.
+     *
+     */
+    uint8_t GetEnhAckFlags(void) const { return mEnhAckFlags; }
+
+    /**
      * This method sets Enhanced ACK Flags.
      *
      * @param[in] aEnhAckFlags  The value of Enhanced ACK Flags.
      *
      */
-    void SetEnhAckFlags(EnhAckFlags aEnhAckFlags)
-    {
-        memcpy(mSubTlvs + kEnhAckFlagsOffset, &aEnhAckFlags, sizeof(aEnhAckFlags));
-    }
+    void SetEnhAckFlags(EnhAckFlags aEnhAckFlags) { mEnhAckFlags = aEnhAckFlags; }
 
     /**
-     * This method sets Type ID Flags.
+     * This method gets the start of Type ID array.
      *
-     * @param[in] aMetrics  A metrics flags to indicate the Type ID Flags.
+     * @returns The start of Type ID array. Array has `kMaxTypeIds` max length.
      *
      */
-    void SetTypeIdFlags(const Metrics &aMetrics)
-    {
-        uint8_t count;
-
-        count = TypeIdFlagsFromMetrics(reinterpret_cast<TypeIdFlags *>(mSubTlvs + kTypeIdFlagsOffset), aMetrics);
-
-        OT_ASSERT(count <= kMaxTypeIdFlagsEnhAck);
-
-        SetLength(sizeof(EnhAckFlags) + sizeof(TypeIdFlags) * count);
-    }
+    uint8_t *GetTypeIds(void) { return mTypeIds; }
 
 private:
-    static constexpr uint8_t  kMaxTypeIdFlagsEnhAck = 3;
-    static constexpr uint8_t  kEnhAckFlagsOffset    = 0;
-    static constexpr uint16_t kTypeIdFlagsOffset    = sizeof(TypeIdFlags);
-
-    uint8_t mSubTlvs[sizeof(EnhAckFlags) + sizeof(TypeIdFlags) * kMaxTypeIdFlagsEnhAck];
+    uint8_t mEnhAckFlags;
+    uint8_t mTypeIds[kMaxTypeIds];
 } OT_TOOL_PACKED_END;
 
 } // namespace LinkMetrics
diff --git a/src/core/thread/link_metrics_types.cpp b/src/core/thread/link_metrics_types.cpp
new file mode 100644
index 0000000..8a5fece
--- /dev/null
+++ b/src/core/thread/link_metrics_types.cpp
@@ -0,0 +1,160 @@
+/*
+ *  Copyright (c) 2020-22, 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 definitions for Thread Link Metrics.
+ */
+
+#include "link_metrics_types.hpp"
+
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+
+#include "common/code_utils.hpp"
+#include "mac/mac_frame.hpp"
+
+namespace ot {
+namespace LinkMetrics {
+
+//----------------------------------------------------------------------------------------------------------------------
+// Metrics
+
+uint8_t Metrics::ConvertToTypeIds(uint8_t aTypeIds[]) const
+{
+    uint8_t count = 0;
+
+    if (mPduCount)
+    {
+        aTypeIds[count++] = TypeId::kPdu;
+    }
+
+    if (mLqi)
+    {
+        aTypeIds[count++] = TypeId::kLqi;
+    }
+
+    if (mLinkMargin)
+    {
+        aTypeIds[count++] = TypeId::kLinkMargin;
+    }
+
+    if (mRssi)
+    {
+        aTypeIds[count++] = TypeId::kRssi;
+    }
+
+#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+    if (mReserved)
+    {
+        for (uint8_t i = 0; i < count; i++)
+        {
+            TypeId::MarkAsReserved(aTypeIds[i]);
+        }
+    }
+#endif
+
+    return count;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// SeriesFlags
+
+uint8_t SeriesFlags::ConvertToMask(void) const
+{
+    uint8_t mask = 0;
+
+    mask |= (mLinkProbe ? kLinkProbeFlag : 0);
+    mask |= (mMacData ? kMacDataFlag : 0);
+    mask |= (mMacDataRequest ? kMacDataRequestFlag : 0);
+    mask |= (mMacAck ? kMacAckFlag : 0);
+
+    return mask;
+}
+
+void SeriesFlags::SetFrom(uint8_t aFlagsMask)
+{
+    mLinkProbe      = (aFlagsMask & kLinkProbeFlag);
+    mMacData        = (aFlagsMask & kMacDataFlag);
+    mMacDataRequest = (aFlagsMask & kMacDataRequestFlag);
+    mMacAck         = (aFlagsMask & kMacAckFlag);
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+// SeriesInfo
+
+void SeriesInfo::Init(uint8_t aSeriesId, uint8_t aSeriesFlagsMask, const Metrics &aMetrics)
+{
+    mSeriesId = aSeriesId;
+    mSeriesFlags.SetFrom(aSeriesFlagsMask);
+    mMetrics = aMetrics;
+    mRssAverager.Clear();
+    mLqiAverager.Clear();
+    mPduCount = 0;
+}
+
+void SeriesInfo::AggregateLinkMetrics(uint8_t aFrameType, uint8_t aLqi, int8_t aRss)
+{
+    if (IsFrameTypeMatch(aFrameType))
+    {
+        mPduCount++;
+        mLqiAverager.Add(aLqi);
+        IgnoreError(mRssAverager.Add(aRss));
+    }
+}
+
+bool SeriesInfo::IsFrameTypeMatch(uint8_t aFrameType) const
+{
+    bool match = false;
+
+    switch (aFrameType)
+    {
+    case kSeriesTypeLinkProbe:
+        VerifyOrExit(!mSeriesFlags.IsMacDataFlagSet()); // Ignore this when Mac Data is accounted
+        match = mSeriesFlags.IsLinkProbeFlagSet();
+        break;
+    case Mac::Frame::kTypeData:
+        match = mSeriesFlags.IsMacDataFlagSet();
+        break;
+    case Mac::Frame::kTypeMacCmd:
+        match = mSeriesFlags.IsMacDataRequestFlagSet();
+        break;
+    case Mac::Frame::kTypeAck:
+        match = mSeriesFlags.IsMacAckFlagSet();
+        break;
+    default:
+        break;
+    }
+
+exit:
+    return match;
+}
+
+} // namespace LinkMetrics
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
diff --git a/src/core/thread/link_metrics_types.hpp b/src/core/thread/link_metrics_types.hpp
new file mode 100644
index 0000000..eb10b2c
--- /dev/null
+++ b/src/core/thread/link_metrics_types.hpp
@@ -0,0 +1,381 @@
+/*
+ *  Copyright (c) 2020-22, 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 definitions for generating and processing Link Metrics TLVs.
+ *
+ */
+
+#ifndef LINK_METRICS_TYPES_HPP_
+#define LINK_METRICS_TYPES_HPP_
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+
+#include <openthread/link_metrics.h>
+
+#include "common/as_core_type.hpp"
+#include "common/clearable.hpp"
+#include "common/encoding.hpp"
+#include "common/message.hpp"
+
+namespace ot {
+namespace LinkMetrics {
+
+constexpr uint8_t kMaxTypeIds = 4; ///< Maximum number of Type IDs in a `Metrics`.
+
+/**
+ * This type represents Link Metric Flags indicating a set of metrics.
+ *
+ * @sa otLinkMetrics
+ *
+ */
+class Metrics : public otLinkMetrics, public Clearable<Metrics>
+{
+public:
+    /**
+     * This method converts the `Metrics` into an array of Type IDs.
+     *
+     * @param[out] aTypeIds   The array of Type IDs to populate. MUST have at least `kMaxTypeIds` elements.
+     *
+     * @returns Number of entries added in the array @p aTypeIds.
+     *
+     */
+    uint8_t ConvertToTypeIds(uint8_t aTypeIds[]) const;
+};
+
+/**
+ * This type represents the results (values) for a set of metrics.
+ *
+ * @sa otLinkMetricsValues
+ *
+ */
+class MetricsValues : public otLinkMetricsValues, public Clearable<MetricsValues>
+{
+public:
+    /**
+     * This method gets the metrics flags.
+     *
+     * @returns The metrics flags.
+     *
+     */
+    Metrics &GetMetrics(void) { return static_cast<Metrics &>(mMetrics); }
+
+    /**
+     * This method gets the metrics flags.
+     *
+     * @returns The metrics flags.
+     *
+     */
+    const Metrics &GetMetrics(void) const { return static_cast<const Metrics &>(mMetrics); }
+
+    /**
+     * This method set the metrics flags.
+     *
+     * @param[in] aMetrics  The metrics flags to set from.
+     *
+     */
+    void SetMetrics(const Metrics &aMetrics) { mMetrics = aMetrics; }
+};
+
+class TypeId
+{
+    // Type ID Flags
+    //
+    //   7   6   5   4   3   2   1   0
+    // +---+---+---+---+---+---+---+---+
+    // | E | L |   Type    |   Metric  |
+    // +---+---+---+---+---+---+---+---+
+    //
+
+    static constexpr uint8_t kExtendedFlag = 1 << 7;
+    static constexpr uint8_t kLengthFlag   = 1 << 6;
+    static constexpr uint8_t kTypeOffset   = 3;
+    static constexpr uint8_t kMetricOffset = 0;
+    static constexpr uint8_t kTypeMask     = (7 << kTypeOffset);
+
+    static constexpr uint8_t kTypeCount    = (0 << kTypeOffset); // Count/summation
+    static constexpr uint8_t kTypeAve      = (1 << kTypeOffset); // Exponential Moving average
+    static constexpr uint8_t kTypeReserved = (2 << kTypeOffset); // Reserved
+
+    static constexpr uint8_t kMetricPdu        = (0 << kMetricOffset); // Number of PDUs received.
+    static constexpr uint8_t kMetricLqi        = (1 << kMetricOffset);
+    static constexpr uint8_t kMetricLinkMargin = (2 << kMetricOffset);
+    static constexpr uint8_t kMetricRssi       = (3 << kMetricOffset);
+
+public:
+    static constexpr uint8_t kPdu        = (kMetricPdu | kTypeCount | kLengthFlag); ///< Type ID for num PDU received.
+    static constexpr uint8_t kLqi        = (kMetricLqi | kTypeAve);                 ///< Type ID for LQI.
+    static constexpr uint8_t kLinkMargin = (kMetricLinkMargin | kTypeAve);          ///< Type ID for Link Margin.
+    static constexpr uint8_t kRssi       = (kMetricRssi | kTypeAve);                ///< Type ID for RSSI.
+
+    /**
+     * This static method indicates whether or not a given Type ID is extended.
+     *
+     * Extended Type IDs are reserved for future use. When set an additional second byte follows the current ID flags.
+     *
+     * @param[in] aTypeId   The Type ID to check.
+     *
+     * @retval TRUE  The @p aTypeId is extended.
+     * @retval FALSE The @p aTypeId is not extended.
+     *
+     */
+    static bool IsExtended(uint8_t aTypeId) { return (aTypeId & kExtendedFlag); }
+
+    /**
+     * This static method determines the value length (number of bytes) associated with a given Type ID.
+     *
+     * Type IDs can either have a short value as a `uint8_t` (e.g., `kLqi`, `kLinkMargin` or `kRssi`) or a long value as
+     * a `uint32_t` (`kPdu`).
+     *
+     * @param[in] aTypeId   The Type ID.
+     *
+     * @returns the associated value length of @p aTypeId.
+     *
+     */
+    static uint8_t GetValueLength(uint8_t aTypeId)
+    {
+        return (aTypeId & kLengthFlag) ? sizeof(uint32_t) : sizeof(uint8_t);
+    }
+
+    /**
+     * This static method updates a Type ID to mark it as reversed.
+     *
+     * This is used for testing only.
+     *
+     * @param[in, out] aTypeId    A reference to a Type ID variable to update.
+     *
+     */
+    static void MarkAsReserved(uint8_t &aTypeId) { aTypeId = (aTypeId & ~kTypeMask) | kTypeReserved; }
+
+    TypeId(void) = delete;
+};
+
+/**
+ * This class represents the Series Flags for Forward Tracking Series.
+ *
+ */
+class SeriesFlags : public otLinkMetricsSeriesFlags
+{
+public:
+    /**
+     * This method converts the `SeriesFlags` to `uint8_t` bit-mask (for inclusion in TLVs).
+     *
+     * @returns The bit-mask representation.
+     *
+     */
+    uint8_t ConvertToMask(void) const;
+
+    /**
+     * This method sets the `SeriesFlags` from a given bit-mask value.
+     *
+     * @param[in] aFlagsMask  The bit-mask flags.
+     *
+     */
+    void SetFrom(uint8_t aFlagsMask);
+
+    /**
+     * This method indicates whether or not the Link Probe flag is set.
+     *
+     * @retval true   The Link Probe flag is set.
+     * @retval false  The Link Probe flag is not set.
+     *
+     */
+    bool IsLinkProbeFlagSet(void) const { return mLinkProbe; }
+
+    /**
+     * This method indicates whether or not the MAC Data flag is set.
+     *
+     * @retval true   The MAC Data flag is set.
+     * @retval false  The MAC Data flag is not set.
+     *
+     */
+    bool IsMacDataFlagSet(void) const { return mMacData; }
+
+    /**
+     * This method indicates whether or not the MAC Data Request flag is set.
+     *
+     * @retval true   The MAC Data Request flag is set.
+     * @retval false  The MAC Data Request flag is not set.
+     *
+     */
+    bool IsMacDataRequestFlagSet(void) const { return mMacDataRequest; }
+
+    /**
+     * This method indicates whether or not the Mac Ack flag is set.
+     *
+     * @retval true   The Mac Ack flag is set.
+     * @retval false  The Mac Ack flag is not set.
+     *
+     */
+    bool IsMacAckFlagSet(void) const { return mMacAck; }
+
+private:
+    static constexpr uint8_t kLinkProbeFlag      = 1 << 0;
+    static constexpr uint8_t kMacDataFlag        = 1 << 1;
+    static constexpr uint8_t kMacDataRequestFlag = 1 << 2;
+    static constexpr uint8_t kMacAckFlag         = 1 << 3;
+};
+
+/**
+ * This enumeration type represent Enhanced-ACK Flags.
+ *
+ */
+enum EnhAckFlags : uint8_t
+{
+    kEnhAckClear    = OT_LINK_METRICS_ENH_ACK_CLEAR,    ///< Clear.
+    kEnhAckRegister = OT_LINK_METRICS_ENH_ACK_REGISTER, ///< Register.
+};
+
+/**
+ * This class represents one Series that is being tracked by the Subject.
+ *
+ * When an Initiator successfully configured a Forward Tracking Series, the Subject would use an instance of this class
+ * to track the information of the Series. The Subject has a `Pool` of `SeriesInfo`. It would allocate one when a new
+ * Series comes, and free it when a Series finishes.
+ *
+ * This class inherits `LinkedListEntry` and each `Neighbor` has a list of `SeriesInfo` so that the Subject could track
+ * per Series initiated by neighbors as long as it has available resources.
+ *
+ */
+class SeriesInfo : public LinkedListEntry<SeriesInfo>
+{
+    friend class LinkedList<SeriesInfo>;
+    friend class LinkedListEntry<SeriesInfo>;
+
+public:
+    /**
+     * This constant represents Link Probe when filtering frames to be accounted using Series Flag. There's
+     * already `Mac::Frame::kTypeData`, `Mac::Frame::kTypeAck` and `Mac::Frame::kTypeMacCmd`. This item is
+     * added so that we can filter a Link Probe for series in the same way as other frames.
+     *
+     */
+    static constexpr uint8_t kSeriesTypeLinkProbe = 0;
+
+    /**
+     * This method initializes the SeriesInfo object.
+     *
+     * @param[in]  aSeriesId          The Series ID.
+     * @param[in]  aSeriesFlagsMask   The Series Flags bitmask which specify what types of frames are to be accounted.
+     * @param[in]  aMetrics           Metrics to query.
+     *
+     */
+    void Init(uint8_t aSeriesId, uint8_t aSeriesFlagsMask, const Metrics &aMetrics);
+
+    /**
+     * This method gets the Series ID.
+     *
+     * @returns  The Series ID.
+     *
+     */
+    uint8_t GetSeriesId(void) const { return mSeriesId; }
+
+    /**
+     * This method gets the PDU count.
+     *
+     * @returns  The PDU count.
+     *
+     */
+    uint32_t GetPduCount(void) const { return mPduCount; }
+
+    /**
+     * This method gets the average LQI.
+     *
+     * @returns  The average LQI.
+     *
+     */
+    uint8_t GetAverageLqi(void) const { return mLqiAverager.GetAverage(); }
+
+    /**
+     * This method gets the average RSS.
+     *
+     * @returns  The average RSS.
+     *
+     */
+    int8_t GetAverageRss(void) const { return mRssAverager.GetAverage(); }
+
+    /**
+     * This method aggregates the Link Metrics data of a frame into this series.
+     *
+     * @param[in]  aFrameType    The type of the frame.
+     * @param[in]  aLqi          The LQI value.
+     * @param[in]  aRss          The RSS value.
+     *
+     */
+    void AggregateLinkMetrics(uint8_t aFrameType, uint8_t aLqi, int8_t aRss);
+
+    /**
+     * This methods gets the metrics.
+     *
+     * @returns  The metrics associated with `SeriesInfo`.
+     *
+     */
+    const Metrics &GetLinkMetrics(void) const { return mMetrics; }
+
+private:
+    bool Matches(const uint8_t &aSeriesId) const { return mSeriesId == aSeriesId; }
+    bool IsFrameTypeMatch(uint8_t aFrameType) const;
+
+    SeriesInfo *mNext;
+    uint8_t     mSeriesId;
+    SeriesFlags mSeriesFlags;
+    Metrics     mMetrics;
+    RssAverager mRssAverager;
+    LqiAverager mLqiAverager;
+    uint32_t    mPduCount;
+};
+
+/**
+ * This enumeration type represents Link Metrics Status.
+ *
+ */
+enum Status : uint8_t
+{
+    kStatusSuccess                   = OT_LINK_METRICS_STATUS_SUCCESS,
+    kStatusCannotSupportNewSeries    = OT_LINK_METRICS_STATUS_CANNOT_SUPPORT_NEW_SERIES,
+    kStatusSeriesIdAlreadyRegistered = OT_LINK_METRICS_STATUS_SERIESID_ALREADY_REGISTERED,
+    kStatusSeriesIdNotRecognized     = OT_LINK_METRICS_STATUS_SERIESID_NOT_RECOGNIZED,
+    kStatusNoMatchingFramesReceived  = OT_LINK_METRICS_STATUS_NO_MATCHING_FRAMES_RECEIVED,
+    kStatusOtherError                = OT_LINK_METRICS_STATUS_OTHER_ERROR,
+};
+
+} // namespace LinkMetrics
+
+DefineCoreType(otLinkMetrics, LinkMetrics::Metrics);
+DefineCoreType(otLinkMetricsValues, LinkMetrics::MetricsValues);
+DefineCoreType(otLinkMetricsSeriesFlags, LinkMetrics::SeriesFlags);
+DefineMapEnum(otLinkMetricsEnhAckFlags, LinkMetrics::EnhAckFlags);
+DefineMapEnum(otLinkMetricsStatus, LinkMetrics::Status);
+
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+
+#endif // LINK_METRICS_TYPES_HPP_
diff --git a/src/core/thread/link_quality.cpp b/src/core/thread/link_quality.cpp
index f3e1cb2..23a68c5 100644
--- a/src/core/thread/link_quality.cpp
+++ b/src/core/thread/link_quality.cpp
@@ -38,6 +38,7 @@
 #include "common/code_utils.hpp"
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
+#include "common/num_utils.hpp"
 
 namespace ot {
 
@@ -63,7 +64,7 @@
     Error    error = kErrorNone;
     uint16_t newValue;
 
-    VerifyOrExit(aRss != OT_RADIO_RSSI_INVALID, error = kErrorInvalidArgs);
+    VerifyOrExit(aRss != Radio::kInvalidRssi, error = kErrorInvalidArgs);
 
     // Restrict the RSS value to the closed range [0, -128] so the RSS times precision multiple can fit in 11 bits.
     if (aRss > 0)
@@ -89,7 +90,7 @@
 {
     int8_t average;
 
-    VerifyOrExit(mCount != 0, average = OT_RADIO_RSSI_INVALID);
+    VerifyOrExit(mCount != 0, average = Radio::kInvalidRssi);
 
     average = -static_cast<int8_t>(mAverage >> kPrecisionBitShift);
 
@@ -123,7 +124,8 @@
     {
         mCount++;
     }
-    count = OT_MIN((1 << kCoeffBitShift), mCount);
+
+    count = Min(static_cast<uint8_t>(1 << kCoeffBitShift), mCount);
 
     mAverage = static_cast<uint8_t>(((mAverage * (count - 1)) + aLqi) / count);
 }
@@ -132,7 +134,7 @@
 {
     mRssAverager.Clear();
     SetLinkQuality(kLinkQuality0);
-    mLastRss = OT_RADIO_RSSI_INVALID;
+    mLastRss = Radio::kInvalidRssi;
 
     mFrameErrorRate.Clear();
     mMessageErrorRate.Clear();
@@ -142,7 +144,7 @@
 {
     uint8_t oldLinkQuality = kNoLinkQuality;
 
-    VerifyOrExit(aRss != OT_RADIO_RSSI_INVALID);
+    VerifyOrExit(aRss != Radio::kInvalidRssi);
 
     mLastRss = aRss;
 
@@ -161,7 +163,7 @@
 
 uint8_t LinkQualityInfo::GetLinkMargin(void) const
 {
-    return ConvertRssToLinkMargin(Get<Mac::SubMac>().GetNoiseFloor(), GetAverageRss());
+    return ComputeLinkMargin(Get<Mac::SubMac>().GetNoiseFloor(), GetAverageRss());
 }
 
 LinkQualityInfo::InfoString LinkQualityInfo::ToInfoString(void) const
@@ -174,11 +176,11 @@
     return string;
 }
 
-uint8_t LinkQualityInfo::ConvertRssToLinkMargin(int8_t aNoiseFloor, int8_t aRss)
+uint8_t ComputeLinkMargin(int8_t aNoiseFloor, int8_t aRss)
 {
     int8_t linkMargin = aRss - aNoiseFloor;
 
-    if (linkMargin < 0 || aRss == OT_RADIO_RSSI_INVALID)
+    if (linkMargin < 0 || aRss == Radio::kInvalidRssi)
     {
         linkMargin = 0;
     }
@@ -186,40 +188,58 @@
     return static_cast<uint8_t>(linkMargin);
 }
 
-LinkQuality LinkQualityInfo::ConvertLinkMarginToLinkQuality(uint8_t aLinkMargin)
+LinkQuality LinkQualityForLinkMargin(uint8_t aLinkMargin)
 {
-    return CalculateLinkQuality(aLinkMargin, kNoLinkQuality);
+    return LinkQualityInfo::CalculateLinkQuality(aLinkMargin, LinkQualityInfo::kNoLinkQuality);
 }
 
-LinkQuality LinkQualityInfo::ConvertRssToLinkQuality(int8_t aNoiseFloor, int8_t aRss)
+int8_t GetTypicalRssForLinkQuality(int8_t aNoiseFloor, LinkQuality aLinkQuality)
 {
-    return ConvertLinkMarginToLinkQuality(ConvertRssToLinkMargin(aNoiseFloor, aRss));
-}
-
-int8_t LinkQualityInfo::ConvertLinkQualityToRss(int8_t aNoiseFloor, LinkQuality aLinkQuality)
-{
-    int8_t linkmargin = 0;
+    int8_t linkMargin = 0;
 
     switch (aLinkQuality)
     {
     case kLinkQuality3:
-        linkmargin = kLinkQuality3LinkMargin;
+        linkMargin = LinkQualityInfo::kLinkQuality3LinkMargin;
         break;
 
     case kLinkQuality2:
-        linkmargin = kLinkQuality2LinkMargin;
+        linkMargin = LinkQualityInfo::kLinkQuality2LinkMargin;
         break;
 
     case kLinkQuality1:
-        linkmargin = kLinkQuality1LinkMargin;
+        linkMargin = LinkQualityInfo::kLinkQuality1LinkMargin;
         break;
 
     default:
-        linkmargin = kLinkQuality0LinkMargin;
+        linkMargin = LinkQualityInfo::kLinkQuality0LinkMargin;
         break;
     }
 
-    return linkmargin + aNoiseFloor;
+    return linkMargin + aNoiseFloor;
+}
+
+uint8_t CostForLinkQuality(LinkQuality aLinkQuality)
+{
+    static const uint8_t kCostsForLinkQuality[] = {
+        kCostForLinkQuality0, // Link cost for `kLinkQuality0` (0).
+        kCostForLinkQuality1, // Link cost for `kLinkQuality1` (1).
+        kCostForLinkQuality2, // Link cost for `kLinkQuality2` (2).
+        kCostForLinkQuality3, // Link cost for `kLinkQuality3` (3).
+    };
+
+    static_assert(kLinkQuality0 == 0, "kLinkQuality0 is invalid");
+    static_assert(kLinkQuality1 == 1, "kLinkQuality1 is invalid");
+    static_assert(kLinkQuality2 == 2, "kLinkQuality2 is invalid");
+    static_assert(kLinkQuality3 == 3, "kLinkQuality3 is invalid");
+
+    uint8_t cost = Mle::kMaxRouteCost;
+
+    VerifyOrExit(aLinkQuality <= kLinkQuality3);
+    cost = kCostsForLinkQuality[aLinkQuality];
+
+exit:
+    return cost;
 }
 
 LinkQuality LinkQualityInfo::CalculateLinkQuality(uint8_t aLinkMargin, uint8_t aLastLinkQuality)
diff --git a/src/core/thread/link_quality.hpp b/src/core/thread/link_quality.hpp
index bc10957..086d747 100644
--- a/src/core/thread/link_quality.hpp
+++ b/src/core/thread/link_quality.hpp
@@ -41,6 +41,7 @@
 #include "common/clearable.hpp"
 #include "common/locator.hpp"
 #include "common/string.hpp"
+#include "thread/mle_types.hpp"
 
 namespace ot {
 
@@ -125,13 +126,13 @@
     /**
      * This method adds a received signal strength (RSS) value to the average.
      *
-     * If @p aRss is OT_RADIO_RSSI_INVALID, it is ignored and error status kErrorInvalidArgs is returned.
+     * If @p aRss is `Radio::kInvalidRssi`, it is ignored and error status kErrorInvalidArgs is returned.
      * The value of RSS is capped at 0dBm (i.e., for any given RSS value higher than 0dBm, 0dBm is used instead).
      *
      * @param[in] aRss                Received signal strength value (in dBm) to be added to the average.
      *
      * @retval kErrorNone         New RSS value added to average successfully.
-     * @retval kErrorInvalidArgs  Value of @p aRss is OT_RADIO_RSSI_INVALID.
+     * @retval kErrorInvalidArgs  Value of @p aRss is `Radio::kInvalidRssi`.
      *
      */
     Error Add(int8_t aRss);
@@ -139,7 +140,7 @@
     /**
      * This method returns the current average signal strength value maintained by the averager.
      *
-     * @returns The current average value (in dBm) or OT_RADIO_RSSI_INVALID if no average is available.
+     * @returns The current average value (in dBm) or `Radio::kInvalidRssi` if no average is available.
      *
      */
     int8_t GetAverage(void) const;
@@ -241,6 +242,53 @@
     kLinkQuality3 = 3, ///< Link quality 3
 };
 
+constexpr uint8_t kCostForLinkQuality0 = Mle::kMaxRouteCost; ///< Link Cost for Link Quality 0.
+constexpr uint8_t kCostForLinkQuality1 = 4;                  ///< Link Cost for Link Quality 1.
+constexpr uint8_t kCostForLinkQuality2 = 2;                  ///< Link Cost for Link Quality 2.
+constexpr uint8_t kCostForLinkQuality3 = 1;                  ///< Link Cost for Link Quality 3.
+
+/**
+ * This function converts link quality to route cost.
+ *
+ * @param[in]  aLinkQuality  The link quality to covert.
+ *
+ * @returns The route cost corresponding to @p aLinkQuality.
+ *
+ */
+uint8_t CostForLinkQuality(LinkQuality aLinkQuality);
+
+/**
+ * This function computes the link margin from a given noise floor and received signal strength.
+ *
+ * @param[in]  aNoiseFloor  The noise floor value (in dBm).
+ * @param[in]  aRss         The received signal strength value (in dBm).
+ *
+ * @returns The link margin value in dB.
+ *
+ */
+uint8_t ComputeLinkMargin(int8_t aNoiseFloor, int8_t aRss);
+
+/**
+ * This function converts a link margin value to a link quality value.
+ *
+ * @param[in]  aLinkMargin  The Link Margin in dB.
+ *
+ * @returns The link quality value (0-3).
+ *
+ */
+LinkQuality LinkQualityForLinkMargin(uint8_t aLinkMargin);
+
+/**
+ * This function gets the typical received signal strength value for a given link quality.
+ *
+ * @param[in]  aNoiseFloor   The noise floor value (in dBm).
+ * @param[in]  aLinkQuality  The link quality value in [0, 3].
+ *
+ * @returns The typical platform RSSI in dBm.
+ *
+ */
+int8_t GetTypicalRssForLinkQuality(int8_t aNoiseFloor, LinkQuality aLinkQuality);
+
 /**
  * This class encapsulates/stores all relevant information about quality of a link, including average received signal
  * strength (RSS), last RSS, link margin, and link quality.
@@ -248,6 +296,9 @@
  */
 class LinkQualityInfo : public InstanceLocatorInit
 {
+    friend LinkQuality LinkQualityForLinkMargin(uint8_t aLinkMargin);
+    friend int8_t      GetTypicalRssForLinkQuality(int8_t aNoiseFloor, LinkQuality aLinkQuality);
+
 public:
     static constexpr uint16_t kInfoStringSize = 50; ///< `InfoString` size (@sa ToInfoString()).
 
@@ -272,6 +323,12 @@
     void Clear(void);
 
     /**
+     * This method clears the average RSS value.
+     *
+     */
+    void ClearAverageRss(void) { mRssAverager.Clear(); }
+
+    /**
      * This method adds a new received signal strength (RSS) value to the average.
      *
      * @param[in] aRss         A new received signal strength value (in dBm) to be added to the average.
@@ -282,7 +339,7 @@
     /**
      * This method returns the current average received signal strength value.
      *
-     * @returns The current average value or @c OT_RADIO_RSSI_INVALID if no average is available.
+     * @returns The current average value or `Radio::kInvalidRssi` if no average is available.
      *
      */
     int8_t GetAverageRss(void) const { return mRssAverager.GetAverage(); }
@@ -387,51 +444,6 @@
      */
     uint16_t GetMessageErrorRate(void) const { return mMessageErrorRate.GetFailureRate(); }
 
-    /**
-     * This method converts a received signal strength value to a link margin value.
-     *
-     * @param[in]  aNoiseFloor  The noise floor value (in dBm).
-     * @param[in]  aRss         The received signal strength value (in dBm).
-     *
-     * @returns The link margin value.
-     *
-     */
-    static uint8_t ConvertRssToLinkMargin(int8_t aNoiseFloor, int8_t aRss);
-
-    /**
-     * This method converts a link margin value to a link quality value.
-     *
-     * @param[in]  aLinkMargin  The Link Margin in dB.
-     *
-     * @returns The link quality value (0-3).
-     *
-     */
-    static LinkQuality ConvertLinkMarginToLinkQuality(uint8_t aLinkMargin);
-
-    /**
-     * This method converts a received signal strength value to a link quality value.
-     *
-     * @param[in]  aNoiseFloor  The noise floor value (in dBm).
-     * @param[in]  aRss         The received signal strength value (in dBm).
-     *
-     * @returns The link quality value (0-3).
-     *
-     */
-    static LinkQuality ConvertRssToLinkQuality(int8_t aNoiseFloor, int8_t aRss);
-
-    /**
-     * This method converts a link quality value to a typical received signal strength value.
-     *
-     * @note only for test.
-     *
-     * @param[in]  aNoiseFloor   The noise floor value (in dBm).
-     * @param[in]  aLinkQuality  The link quality value in [0, 3].
-     *
-     * @returns The typical platform RSSI.
-     *
-     */
-    static int8_t ConvertLinkQualityToRss(int8_t aNoiseFloor, LinkQuality aLinkQuality);
-
 private:
     // Constants for obtaining link quality from link margin:
 
diff --git a/src/core/thread/lowpan.cpp b/src/core/thread/lowpan.cpp
index d373fbe..a31519b 100644
--- a/src/core/thread/lowpan.cpp
+++ b/src/core/thread/lowpan.cpp
@@ -45,7 +45,6 @@
 
 using ot::Encoding::BigEndian::HostSwap16;
 using ot::Encoding::BigEndian::ReadUint16;
-using ot::Encoding::BigEndian::WriteUint16;
 
 namespace ot {
 namespace Lowpan {
@@ -55,37 +54,43 @@
 {
 }
 
-void Lowpan::CopyContext(const Context &aContext, Ip6::Address &aAddress)
+void Lowpan::FindContextForId(uint8_t aContextId, Context &aContext) const
 {
-    aAddress.SetPrefix(aContext.mPrefix);
+    if (Get<NetworkData::Leader>().GetContext(aContextId, aContext) != kErrorNone)
+    {
+        aContext.Clear();
+    }
 }
 
-Error Lowpan::ComputeIid(const Mac::Address &aMacAddr, const Context &aContext, Ip6::Address &aIpAddress)
+void Lowpan::FindContextToCompressAddress(const Ip6::Address &aIp6Address, Context &aContext) const
+{
+    Error error = Get<NetworkData::Leader>().GetContext(aIp6Address, aContext);
+
+    if ((error != kErrorNone) || !aContext.mCompressFlag)
+    {
+        aContext.Clear();
+    }
+}
+
+Error Lowpan::ComputeIid(const Mac::Address &aMacAddr, const Context &aContext, Ip6::InterfaceIdentifier &aIid)
 {
     Error error = kErrorNone;
 
     switch (aMacAddr.GetType())
     {
     case Mac::Address::kTypeShort:
-        aIpAddress.GetIid().SetToLocator(aMacAddr.GetShort());
+        aIid.SetToLocator(aMacAddr.GetShort());
         break;
 
     case Mac::Address::kTypeExtended:
-        aIpAddress.GetIid().SetFromExtAddress(aMacAddr.GetExtended());
+        aIid.SetFromExtAddress(aMacAddr.GetExtended());
         break;
 
     default:
         ExitNow(error = kErrorParse);
     }
 
-    if (aContext.mPrefix.GetLength() > 64)
-    {
-        for (int i = (aContext.mPrefix.GetLength() & ~7); i < aContext.mPrefix.GetLength(); i++)
-        {
-            aIpAddress.mFields.m8[i / CHAR_BIT] &= ~(0x80 >> (i % CHAR_BIT));
-            aIpAddress.mFields.m8[i / CHAR_BIT] |= aContext.mPrefix.GetBytes()[i / CHAR_BIT] & (0x80 >> (i % CHAR_BIT));
-        }
-    }
+    aIid.ApplyPrefix(aContext.mPrefix);
 
 exit:
     return error;
@@ -93,75 +98,59 @@
 
 Error Lowpan::CompressSourceIid(const Mac::Address &aMacAddr,
                                 const Ip6::Address &aIpAddr,
-                                const Context &     aContext,
-                                uint16_t &          aHcCtl,
-                                FrameBuilder &      aFrameBuilder)
+                                const Context      &aContext,
+                                uint16_t           &aHcCtl,
+                                FrameBuilder       &aFrameBuilder)
 {
-    Error        error = kErrorNone;
-    Ip6::Address ipaddr;
-    Mac::Address tmp;
+    Error                    error = kErrorNone;
+    Ip6::InterfaceIdentifier iid;
 
-    IgnoreError(ComputeIid(aMacAddr, aContext, ipaddr));
+    IgnoreError(ComputeIid(aMacAddr, aContext, iid));
 
-    if (ipaddr.GetIid() == aIpAddr.GetIid())
+    if (iid == aIpAddr.GetIid())
     {
         aHcCtl |= kHcSrcAddrMode3;
     }
+    else if (aIpAddr.GetIid().IsLocator())
+    {
+        aHcCtl |= kHcSrcAddrMode2;
+        error = aFrameBuilder.AppendBigEndianUint16(aIpAddr.GetIid().GetLocator());
+    }
     else
     {
-        tmp.SetShort(aIpAddr.GetIid().GetLocator());
-        IgnoreError(ComputeIid(tmp, aContext, ipaddr));
-
-        if (ipaddr.GetIid() == aIpAddr.GetIid())
-        {
-            aHcCtl |= kHcSrcAddrMode2;
-            SuccessOrExit(error = aFrameBuilder.AppendBytes(aIpAddr.mFields.m8 + 14, 2));
-        }
-        else
-        {
-            aHcCtl |= kHcSrcAddrMode1;
-            SuccessOrExit(error = aFrameBuilder.Append(aIpAddr.GetIid()));
-        }
+        aHcCtl |= kHcSrcAddrMode1;
+        error = aFrameBuilder.Append(aIpAddr.GetIid());
     }
 
-exit:
     return error;
 }
 
 Error Lowpan::CompressDestinationIid(const Mac::Address &aMacAddr,
                                      const Ip6::Address &aIpAddr,
-                                     const Context &     aContext,
-                                     uint16_t &          aHcCtl,
-                                     FrameBuilder &      aFrameBuilder)
+                                     const Context      &aContext,
+                                     uint16_t           &aHcCtl,
+                                     FrameBuilder       &aFrameBuilder)
 {
-    Error        error = kErrorNone;
-    Ip6::Address ipaddr;
-    Mac::Address tmp;
+    Error                    error = kErrorNone;
+    Ip6::InterfaceIdentifier iid;
 
-    IgnoreError(ComputeIid(aMacAddr, aContext, ipaddr));
+    IgnoreError(ComputeIid(aMacAddr, aContext, iid));
 
-    if (ipaddr.GetIid() == aIpAddr.GetIid())
+    if (iid == aIpAddr.GetIid())
     {
         aHcCtl |= kHcDstAddrMode3;
     }
+    else if (aIpAddr.GetIid().IsLocator())
+    {
+        aHcCtl |= kHcDstAddrMode2;
+        error = aFrameBuilder.AppendBigEndianUint16(aIpAddr.GetIid().GetLocator());
+    }
     else
     {
-        tmp.SetShort(aIpAddr.GetIid().GetLocator());
-        IgnoreError(ComputeIid(tmp, aContext, ipaddr));
-
-        if (ipaddr.GetIid() == aIpAddr.GetIid())
-        {
-            aHcCtl |= kHcDstAddrMode2;
-            SuccessOrExit(error = aFrameBuilder.AppendBytes(aIpAddr.mFields.m8 + 14, 2));
-        }
-        else
-        {
-            aHcCtl |= kHcDstAddrMode1;
-            SuccessOrExit(error = aFrameBuilder.Append(aIpAddr.GetIid()));
-        }
+        aHcCtl |= kHcDstAddrMode1;
+        error = aFrameBuilder.Append(aIpAddr.GetIid());
     }
 
-exit:
     return error;
 }
 
@@ -198,9 +187,10 @@
             }
             else
             {
-                // Check if multicast address can be compressed using Context ID 0.
-                if (Get<NetworkData::Leader>().GetContext(0, multicastContext) == kErrorNone &&
-                    multicastContext.mPrefix.GetLength() == aIpAddr.mFields.m8[3] &&
+                // Check if multicast address can be compressed using Context ID 0 (mesh local prefix)
+                FindContextForId(0, multicastContext);
+
+                if (multicastContext.mPrefix.GetLength() == aIpAddr.mFields.m8[3] &&
                     memcmp(multicastContext.mPrefix.GetBytes(), aIpAddr.mFields.m8 + 4, 8) == 0)
                 {
                     aHcCtl |= kHcDstAddrContext | kHcDstAddrMode0;
@@ -221,10 +211,7 @@
     return error;
 }
 
-Error Lowpan::Compress(Message &           aMessage,
-                       const Mac::Address &aMacSource,
-                       const Mac::Address &aMacDest,
-                       FrameBuilder &      aFrameBuilder)
+Error Lowpan::Compress(Message &aMessage, const Mac::Addresses &aMacAddrs, FrameBuilder &aFrameBuilder)
 {
     Error   error       = kErrorNone;
     uint8_t headerDepth = 0xff;
@@ -233,7 +220,7 @@
     {
         FrameBuilder frameBuilder = aFrameBuilder;
 
-        error = Compress(aMessage, aMacSource, aMacDest, aFrameBuilder, headerDepth);
+        error = Compress(aMessage, aMacAddrs, aFrameBuilder, headerDepth);
 
         // We exit if `Compress()` is successful. Otherwise we reset
         // the `aFrameBuidler` to its earlier state (remove all
@@ -248,44 +235,28 @@
     return error;
 }
 
-Error Lowpan::Compress(Message &           aMessage,
-                       const Mac::Address &aMacSource,
-                       const Mac::Address &aMacDest,
-                       FrameBuilder &      aFrameBuilder,
-                       uint8_t &           aHeaderDepth)
+Error Lowpan::Compress(Message              &aMessage,
+                       const Mac::Addresses &aMacAddrs,
+                       FrameBuilder         &aFrameBuilder,
+                       uint8_t              &aHeaderDepth)
 {
-    Error                error       = kErrorNone;
-    NetworkData::Leader &networkData = Get<NetworkData::Leader>();
-    uint16_t             startOffset = aMessage.GetOffset();
-    uint16_t             hcCtl       = kHcDispatch;
-    uint16_t             hcCtlOffset = 0;
-    Ip6::Header          ip6Header;
-    uint8_t *            ip6HeaderBytes = reinterpret_cast<uint8_t *>(&ip6Header);
-    Context              srcContext, dstContext;
-    bool                 srcContextValid, dstContextValid;
-    uint8_t              nextHeader;
-    uint8_t              ecn;
-    uint8_t              dscp;
-    uint8_t              headerDepth    = 0;
-    uint8_t              headerMaxDepth = aHeaderDepth;
+    Error       error       = kErrorNone;
+    uint16_t    startOffset = aMessage.GetOffset();
+    uint16_t    hcCtl       = kHcDispatch;
+    uint16_t    hcCtlOffset = 0;
+    Ip6::Header ip6Header;
+    uint8_t    *ip6HeaderBytes = reinterpret_cast<uint8_t *>(&ip6Header);
+    Context     srcContext, dstContext;
+    uint8_t     nextHeader;
+    uint8_t     ecn;
+    uint8_t     dscp;
+    uint8_t     headerDepth    = 0;
+    uint8_t     headerMaxDepth = aHeaderDepth;
 
     SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), ip6Header));
 
-    srcContextValid =
-        (networkData.GetContext(ip6Header.GetSource(), srcContext) == kErrorNone && srcContext.mCompressFlag);
-
-    if (!srcContextValid)
-    {
-        IgnoreError(networkData.GetContext(0, srcContext));
-    }
-
-    dstContextValid =
-        (networkData.GetContext(ip6Header.GetDestination(), dstContext) == kErrorNone && dstContext.mCompressFlag);
-
-    if (!dstContextValid)
-    {
-        IgnoreError(networkData.GetContext(0, dstContext));
-    }
+    FindContextToCompressAddress(ip6Header.GetSource(), srcContext);
+    FindContextToCompressAddress(ip6Header.GetDestination(), dstContext);
 
     // Lowpan HC Control Bits
     hcCtlOffset = aFrameBuilder.GetLength();
@@ -378,12 +349,14 @@
     }
     else if (ip6Header.GetSource().IsLinkLocal())
     {
-        SuccessOrExit(error = CompressSourceIid(aMacSource, ip6Header.GetSource(), srcContext, hcCtl, aFrameBuilder));
+        SuccessOrExit(
+            error = CompressSourceIid(aMacAddrs.mSource, ip6Header.GetSource(), srcContext, hcCtl, aFrameBuilder));
     }
-    else if (srcContextValid)
+    else if (srcContext.mIsValid)
     {
         hcCtl |= kHcSrcAddrContext;
-        SuccessOrExit(error = CompressSourceIid(aMacSource, ip6Header.GetSource(), srcContext, hcCtl, aFrameBuilder));
+        SuccessOrExit(
+            error = CompressSourceIid(aMacAddrs.mSource, ip6Header.GetSource(), srcContext, hcCtl, aFrameBuilder));
     }
     else
     {
@@ -397,14 +370,14 @@
     }
     else if (ip6Header.GetDestination().IsLinkLocal())
     {
-        SuccessOrExit(
-            error = CompressDestinationIid(aMacDest, ip6Header.GetDestination(), dstContext, hcCtl, aFrameBuilder));
+        SuccessOrExit(error = CompressDestinationIid(aMacAddrs.mDestination, ip6Header.GetDestination(), dstContext,
+                                                     hcCtl, aFrameBuilder));
     }
-    else if (dstContextValid)
+    else if (dstContext.mIsValid)
     {
         hcCtl |= kHcDstAddrContext;
-        SuccessOrExit(
-            error = CompressDestinationIid(aMacDest, ip6Header.GetDestination(), dstContext, hcCtl, aFrameBuilder));
+        SuccessOrExit(error = CompressDestinationIid(aMacAddrs.mDestination, ip6Header.GetDestination(), dstContext,
+                                                     hcCtl, aFrameBuilder));
     }
     else
     {
@@ -433,7 +406,7 @@
             // For IP-in-IP the NH bit of the LOWPAN_NHC encoding MUST be set to zero.
             SuccessOrExit(error = aFrameBuilder.AppendUint8(kExtHdrDispatch | kExtHdrEidIp6));
 
-            error = Compress(aMessage, aMacSource, aMacDest, aFrameBuilder);
+            error = Compress(aMessage, aMacAddrs, aFrameBuilder);
 
             OT_FALL_THROUGH;
 
@@ -465,7 +438,7 @@
     uint16_t             startOffset = aMessage.GetOffset();
     Ip6::ExtensionHeader extHeader;
     uint16_t             len;
-    uint8_t              padLength = 0;
+    uint16_t             padLength = 0;
     uint8_t              tmpByte;
 
     SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), extHeader));
@@ -488,7 +461,7 @@
 
     SuccessOrExit(error = aFrameBuilder.AppendUint8(tmpByte));
 
-    len = (extHeader.GetLength() + 1) * 8 - sizeof(extHeader);
+    len = extHeader.GetSize() - sizeof(extHeader);
 
     // RFC 6282 does not support compressing large extension headers
     VerifyOrExit(len <= kExtHdrMaxLength, error = kErrorFailed);
@@ -499,34 +472,23 @@
     // Pad1 or PadN option MAY be elided by the compressor."
     if (aNextHeader == Ip6::kProtoHopOpts || aNextHeader == Ip6::kProtoDstOpts)
     {
-        uint16_t          offset = aMessage.GetOffset();
-        Ip6::OptionHeader optionHeader;
+        uint16_t    offset    = aMessage.GetOffset();
+        uint16_t    endOffset = offset + len;
+        bool        hasOption = false;
+        Ip6::Option option;
 
-        while ((offset - aMessage.GetOffset()) < len)
+        for (; offset < endOffset; offset += option.GetSize())
         {
-            SuccessOrExit(error = aMessage.Read(offset, optionHeader));
-
-            if (optionHeader.GetType() == Ip6::OptionPad1::kType)
-            {
-                offset += sizeof(Ip6::OptionPad1);
-            }
-            else
-            {
-                offset += sizeof(optionHeader) + optionHeader.GetLength();
-            }
+            SuccessOrExit(error = option.ParseFrom(aMessage, offset, endOffset));
+            hasOption = true;
         }
 
         // Check if the last option can be compressed.
-        if (optionHeader.GetType() == Ip6::OptionPad1::kType)
+        if (hasOption && option.IsPadding())
         {
-            padLength = sizeof(Ip6::OptionPad1);
+            padLength = option.GetSize();
+            len -= padLength;
         }
-        else if (optionHeader.GetType() == Ip6::OptionPadN::kType)
-        {
-            padLength = sizeof(optionHeader) + optionHeader.GetLength();
-        }
-
-        len -= padLength;
     }
 
     VerifyOrExit(aMessage.GetOffset() + len + padLength <= aMessage.GetLength(), error = kErrorParse);
@@ -636,19 +598,19 @@
     return error;
 }
 
-Error Lowpan::DecompressBaseHeader(Ip6::Header &       aIp6Header,
-                                   bool &              aCompressedNextHeader,
-                                   const Mac::Address &aMacSource,
-                                   const Mac::Address &aMacDest,
-                                   FrameData &         aFrameData)
+Error Lowpan::DecompressBaseHeader(Ip6::Header          &aIp6Header,
+                                   bool                 &aCompressedNextHeader,
+                                   const Mac::Addresses &aMacAddrs,
+                                   FrameData            &aFrameData)
 {
-    NetworkData::Leader &networkData = Get<NetworkData::Leader>();
-    Error                error       = kErrorParse;
-    uint16_t             hcCtl;
-    uint8_t              byte;
-    Context              srcContext, dstContext;
-    bool                 srcContextValid = true, dstContextValid = true;
-    uint8_t              nextHeader;
+    Error    error = kErrorParse;
+    uint16_t hcCtl;
+    uint8_t  byte;
+    uint8_t  srcContextId = 0;
+    uint8_t  dstContextId = 0;
+    Context  srcContext;
+    Context  dstContext;
+    uint8_t  nextHeader;
 
     SuccessOrExit(aFrameData.ReadBigEndianUint16(hcCtl));
 
@@ -656,28 +618,16 @@
     VerifyOrExit((hcCtl & kHcDispatchMask) == kHcDispatch);
 
     // Context Identifier
-    srcContext.mPrefix.SetLength(0);
-    dstContext.mPrefix.SetLength(0);
-
     if ((hcCtl & kHcContextId) != 0)
     {
         SuccessOrExit(aFrameData.ReadUint8(byte));
 
-        if (networkData.GetContext(byte >> 4, srcContext) != kErrorNone)
-        {
-            srcContextValid = false;
-        }
+        srcContextId = (byte >> 4);
+        dstContextId = (byte & 0xf);
+    }
 
-        if (networkData.GetContext(byte & 0xf, dstContext) != kErrorNone)
-        {
-            dstContextValid = false;
-        }
-    }
-    else
-    {
-        IgnoreError(networkData.GetContext(0, srcContext));
-        IgnoreError(networkData.GetContext(0, dstContext));
-    }
+    FindContextForId(srcContextId, srcContext);
+    FindContextForId(dstContextId, dstContext);
 
     aIp6Header.Clear();
     aIp6Header.InitVersionTrafficClassFlow();
@@ -764,7 +714,7 @@
         break;
 
     case kHcSrcAddrMode3:
-        IgnoreError(ComputeIid(aMacSource, srcContext, aIp6Header.GetSource()));
+        IgnoreError(ComputeIid(aMacAddrs.mSource, srcContext, aIp6Header.GetSource().GetIid()));
         break;
     }
 
@@ -776,8 +726,8 @@
         }
         else
         {
-            VerifyOrExit(srcContextValid);
-            CopyContext(srcContext, aIp6Header.GetSource());
+            VerifyOrExit(srcContext.mIsValid);
+            aIp6Header.GetSource().SetPrefix(srcContext.mPrefix);
         }
     }
 
@@ -803,7 +753,7 @@
             break;
 
         case kHcDstAddrMode3:
-            SuccessOrExit(ComputeIid(aMacDest, dstContext, aIp6Header.GetDestination()));
+            SuccessOrExit(ComputeIid(aMacAddrs.mDestination, dstContext, aIp6Header.GetDestination().GetIid()));
             break;
         }
 
@@ -816,8 +766,8 @@
         }
         else
         {
-            VerifyOrExit(dstContextValid);
-            CopyContext(dstContext, aIp6Header.GetDestination());
+            VerifyOrExit(dstContext.mIsValid);
+            aIp6Header.GetDestination().SetPrefix(dstContext.mPrefix);
         }
     }
     else
@@ -855,7 +805,7 @@
             switch (hcCtl & kHcDstAddrModeMask)
             {
             case 0:
-                VerifyOrExit(dstContextValid);
+                VerifyOrExit(dstContext.mIsValid);
                 SuccessOrExit(aFrameData.ReadBytes(aIp6Header.GetDestination().mFields.m8 + 1, 2));
                 aIp6Header.GetDestination().mFields.m8[3] = dstContext.mPrefix.GetLength();
                 memcpy(aIp6Header.GetDestination().mFields.m8 + 4, dstContext.mPrefix.GetBytes(), 8);
@@ -883,11 +833,11 @@
 
 Error Lowpan::DecompressExtensionHeader(Message &aMessage, FrameData &aFrameData)
 {
-    Error   error = kErrorParse;
-    uint8_t hdr[2];
-    uint8_t len;
-    uint8_t ctl;
-    uint8_t padLength;
+    Error          error = kErrorParse;
+    uint8_t        hdr[2];
+    uint8_t        len;
+    uint8_t        ctl;
+    Ip6::PadOption padOption;
 
     SuccessOrExit(aFrameData.ReadUint8(ctl));
 
@@ -921,26 +871,11 @@
     // The RFC6282 says: "The trailing Pad1 or PadN option MAY be elided by the compressor.
     // A decompressor MUST ensure that the containing header is padded out to a multiple of 8 octets
     // in length, using a Pad1 or PadN option if necessary."
-    padLength = 8 - ((len + sizeof(hdr)) & 0x07);
 
-    if (padLength != 8)
+    if (padOption.InitToPadHeaderWithSize(len + sizeof(hdr)) == kErrorNone)
     {
-        if (padLength == 1)
-        {
-            Ip6::OptionPad1 optionPad1;
-
-            optionPad1.Init();
-            SuccessOrExit(aMessage.AppendBytes(&optionPad1, padLength));
-        }
-        else
-        {
-            Ip6::OptionPadN optionPadN;
-
-            optionPadN.Init(padLength);
-            SuccessOrExit(aMessage.AppendBytes(&optionPadN, padLength));
-        }
-
-        aMessage.MoveOffset(padLength);
+        SuccessOrExit(aMessage.AppendBytes(&padOption, padOption.GetSize()));
+        aMessage.MoveOffset(padOption.GetSize());
     }
 
     error = kErrorNone;
@@ -1034,11 +969,10 @@
     return error;
 }
 
-Error Lowpan::Decompress(Message &           aMessage,
-                         const Mac::Address &aMacSource,
-                         const Mac::Address &aMacDest,
-                         FrameData &         aFrameData,
-                         uint16_t            aDatagramLength)
+Error Lowpan::Decompress(Message              &aMessage,
+                         const Mac::Addresses &aMacAddrs,
+                         FrameData            &aFrameData,
+                         uint16_t              aDatagramLength)
 {
     Error       error = kErrorParse;
     Ip6::Header ip6Header;
@@ -1046,7 +980,7 @@
     uint16_t    ip6PayloadLength;
     uint16_t    currentOffset = aMessage.GetOffset();
 
-    SuccessOrExit(DecompressBaseHeader(ip6Header, compressed, aMacSource, aMacDest, aFrameData));
+    SuccessOrExit(DecompressBaseHeader(ip6Header, compressed, aMacAddrs, aFrameData));
 
     SuccessOrExit(aMessage.Append(ip6Header));
     aMessage.MoveOffset(sizeof(ip6Header));
@@ -1066,7 +1000,7 @@
 
                 aFrameData.SkipOver(sizeof(uint8_t));
 
-                SuccessOrExit(Decompress(aMessage, aMacSource, aMacDest, aFrameData, aDatagramLength));
+                SuccessOrExit(Decompress(aMessage, aMacAddrs, aFrameData, aDatagramLength));
             }
             else
             {
@@ -1232,50 +1166,43 @@
     }
 }
 
-uint16_t MeshHeader::WriteTo(uint8_t *aFrame) const
+Error MeshHeader::AppendTo(FrameBuilder &aFrameBuilder) const
 {
-    uint8_t *cur      = aFrame;
-    uint8_t  dispatch = (kDispatch | kSourceShort | kDestShort);
+    Error   error;
+    uint8_t dispatch = (kDispatch | kSourceShort | kDestShort);
 
     if (mHopsLeft < kDeepHopsLeft)
     {
-        *cur++ = (dispatch | mHopsLeft);
+        SuccessOrExit(error = aFrameBuilder.AppendUint8(dispatch | mHopsLeft));
     }
     else
     {
-        *cur++ = (dispatch | kDeepHopsLeft);
-        *cur++ = mHopsLeft;
+        SuccessOrExit(error = aFrameBuilder.AppendUint8(dispatch | kDeepHopsLeft));
+        SuccessOrExit(error = aFrameBuilder.AppendUint8(mHopsLeft));
     }
 
-    WriteUint16(mSource, cur);
-    cur += sizeof(uint16_t);
+    SuccessOrExit(error = aFrameBuilder.AppendBigEndianUint16(mSource));
+    SuccessOrExit(error = aFrameBuilder.AppendBigEndianUint16(mDestination));
 
-    WriteUint16(mDestination, cur);
-    cur += sizeof(uint16_t);
-
-    return static_cast<uint16_t>(cur - aFrame);
+exit:
+    return error;
 }
 
 Error MeshHeader::AppendTo(Message &aMessage) const
 {
-    uint8_t  frame[kDeepHopsHeaderLength];
-    uint16_t headerLength;
+    uint8_t      frame[kDeepHopsHeaderLength];
+    FrameBuilder frameBuilder;
 
-    headerLength = WriteTo(frame);
+    frameBuilder.Init(frame, sizeof(frame));
 
-    return aMessage.AppendBytes(frame, headerLength);
+    IgnoreError(AppendTo(frameBuilder));
+
+    return aMessage.AppendBytes(frameBuilder.GetBytes(), frameBuilder.GetLength());
 }
 
 //---------------------------------------------------------------------------------------------------------------------
 // FragmentHeader
 
-void FragmentHeader::Init(uint16_t aSize, uint16_t aTag, uint16_t aOffset)
-{
-    mSize   = (aSize & kSizeMask);
-    mTag    = aTag;
-    mOffset = (aOffset & kOffsetMask);
-}
-
 bool FragmentHeader::IsFragmentHeader(const FrameData &aFrameData)
 {
     return IsFragmentHeader(aFrameData.GetBytes(), aFrameData.GetLength());
@@ -1283,7 +1210,7 @@
 
 bool FragmentHeader::IsFragmentHeader(const uint8_t *aFrame, uint16_t aFrameLength)
 {
-    return (aFrameLength >= kFirstFragmentHeaderSize) && ((*aFrame & kDispatchMask) == kDispatch);
+    return (aFrameLength >= sizeof(FirstFrag)) && ((*aFrame & kDispatchMask) == kDispatch);
 }
 
 Error FragmentHeader::ParseFrom(FrameData &aFrameData)
@@ -1309,14 +1236,14 @@
 
     if ((*aFrame & kOffsetFlag) == kOffsetFlag)
     {
-        VerifyOrExit(aFrameLength >= kSubsequentFragmentHeaderSize);
+        VerifyOrExit(aFrameLength >= sizeof(NextFrag));
         mOffset       = aFrame[kOffsetIndex] * 8;
-        aHeaderLength = kSubsequentFragmentHeaderSize;
+        aHeaderLength = sizeof(NextFrag);
     }
     else
     {
         mOffset       = 0;
-        aHeaderLength = kFirstFragmentHeaderSize;
+        aHeaderLength = sizeof(FirstFrag);
     }
 
     error = kErrorNone;
@@ -1327,7 +1254,7 @@
 
 Error FragmentHeader::ParseFrom(const Message &aMessage, uint16_t aOffset, uint16_t &aHeaderLength)
 {
-    uint8_t  frame[kSubsequentFragmentHeaderSize];
+    uint8_t  frame[sizeof(NextFrag)];
     uint16_t frameLength;
 
     frameLength = aMessage.ReadBytes(aOffset, frame, sizeof(frame));
@@ -1335,24 +1262,5 @@
     return ParseFrom(frame, frameLength, aHeaderLength);
 }
 
-uint16_t FragmentHeader::WriteTo(uint8_t *aFrame) const
-{
-    uint8_t *cur = aFrame;
-
-    WriteUint16((static_cast<uint16_t>(kDispatch) << 8) + mSize, cur);
-    cur += sizeof(uint16_t);
-
-    WriteUint16(mTag, cur);
-    cur += sizeof(uint16_t);
-
-    if (mOffset != 0)
-    {
-        *aFrame |= kOffsetFlag;
-        *cur++ = static_cast<uint8_t>(mOffset >> 3);
-    }
-
-    return static_cast<uint16_t>(cur - aFrame);
-}
-
 } // namespace Lowpan
 } // namespace ot
diff --git a/src/core/thread/lowpan.hpp b/src/core/thread/lowpan.hpp
index 7193607..77139a6 100644
--- a/src/core/thread/lowpan.hpp
+++ b/src/core/thread/lowpan.hpp
@@ -36,6 +36,7 @@
 
 #include "openthread-core-config.h"
 
+#include "common/clearable.hpp"
 #include "common/debug.hpp"
 #include "common/frame_builder.hpp"
 #include "common/frame_data.hpp"
@@ -73,11 +74,12 @@
  * This structure represents a LOWPAN_IPHC Context.
  *
  */
-struct Context
+struct Context : public Clearable<Context>
 {
     Ip6::Prefix mPrefix;       ///< The Prefix
     uint8_t     mContextId;    ///< The Context ID.
     bool        mCompressFlag; ///< The Context compression flag.
+    bool        mIsValid;      ///< Indicates whether the context is valid.
 };
 
 /**
@@ -125,17 +127,13 @@
      * This method compresses an IPv6 header.
      *
      * @param[in]   aMessage       A reference to the IPv6 message.
-     * @param[in]   aMacSource     The MAC source address.
-     * @param[in]   aMacDest       The MAC destination address.
-     * @param[in]  aFrameBuilder   The `FrameBuilder` to use to append the compressed headers.
+     * @param[in]   aMacAddrs      The MAC source and destination addresses.
+     * @param[in]   aFrameBuilder  The `FrameBuilder` to use to append the compressed headers.
      *
      * @returns The size of the compressed header in bytes.
      *
      */
-    Error Compress(Message &           aMessage,
-                   const Mac::Address &aMacSource,
-                   const Mac::Address &aMacDest,
-                   FrameBuilder &      aFrameBuilder);
+    Error Compress(Message &aMessage, const Mac::Addresses &aMacAddrs, FrameBuilder &aFrameBuilder);
 
     /**
      * This method decompresses a LOWPAN_IPHC header.
@@ -143,8 +141,7 @@
      * If the header is parsed successfully the @p aFrameData is updated to skip over the parsed header bytes.
      *
      * @param[out]    aMessage         A reference where the IPv6 header will be placed.
-     * @param[in]     aMacSource       The MAC source address.
-     * @param[in]     aMacDest         The MAC destination address.
+     * @param[in]     aMacAddrs        The MAC source and destination addresses.
      * @param[in,out] aFrameData       A frame data containing the LOWPAN_IPHC header.
      * @param[in]     aDatagramLength  The IPv6 datagram length.
      *
@@ -153,11 +150,10 @@
      * @retval kErrorNoBufs  Could not grow @p aMessage to write the parsed IPv6 header.
      *
      */
-    Error Decompress(Message &           aMessage,
-                     const Mac::Address &aMacSource,
-                     const Mac::Address &aMacDest,
-                     FrameData &         aFrameData,
-                     uint16_t            aDatagramLength);
+    Error Decompress(Message              &aMessage,
+                     const Mac::Addresses &aMacAddrs,
+                     FrameData            &aFrameData,
+                     uint16_t              aDatagramLength);
 
     /**
      * This method decompresses a LOWPAN_IPHC header.
@@ -166,19 +162,17 @@
      *
      * @param[out]    aIp6Header             A reference where the IPv6 header will be placed.
      * @param[out]    aCompressedNextHeader  A boolean reference to output whether next header is compressed or not.
-     * @param[in]     aMacSource             The MAC source address.
-     * @param[in]     aMacDest               The MAC destination address.
+     * @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.
      *
      */
-    Error DecompressBaseHeader(Ip6::Header &       aIp6Header,
-                               bool &              aCompressedNextHeader,
-                               const Mac::Address &aMacSource,
-                               const Mac::Address &aMacDest,
-                               FrameData &         aFrameData);
+    Error DecompressBaseHeader(Ip6::Header          &aIp6Header,
+                               bool                 &aCompressedNextHeader,
+                               const Mac::Addresses &aMacAddrs,
+                               FrameData            &aFrameData);
 
     /**
      * This method decompresses a LOWPAN_NHC UDP header.
@@ -268,23 +262,24 @@
     static constexpr uint8_t kUdpChecksum = 1 << 2;
     static constexpr uint8_t kUdpPortMask = 3 << 0;
 
-    Error Compress(Message &           aMessage,
-                   const Mac::Address &aMacSource,
-                   const Mac::Address &aMacDest,
-                   FrameBuilder &      aFrameBuilder,
-                   uint8_t &           aHeaderDepth);
+    void  FindContextForId(uint8_t aContextId, Context &aContext) const;
+    void  FindContextToCompressAddress(const Ip6::Address &aIp6Address, Context &aContext) const;
+    Error Compress(Message              &aMessage,
+                   const Mac::Addresses &aMacAddrs,
+                   FrameBuilder         &aFrameBuilder,
+                   uint8_t              &aHeaderDepth);
 
     Error CompressExtensionHeader(Message &aMessage, FrameBuilder &aFrameBuilder, uint8_t &aNextHeader);
     Error CompressSourceIid(const Mac::Address &aMacAddr,
                             const Ip6::Address &aIpAddr,
-                            const Context &     aContext,
-                            uint16_t &          aHcCtl,
-                            FrameBuilder &      aFrameBuilder);
+                            const Context      &aContext,
+                            uint16_t           &aHcCtl,
+                            FrameBuilder       &aFrameBuilder);
     Error CompressDestinationIid(const Mac::Address &aMacAddr,
                                  const Ip6::Address &aIpAddr,
-                                 const Context &     aContext,
-                                 uint16_t &          aHcCtl,
-                                 FrameBuilder &      aFrameBuilder);
+                                 const Context      &aContext,
+                                 uint16_t           &aHcCtl,
+                                 FrameBuilder       &aFrameBuilder);
     Error CompressMulticast(const Ip6::Address &aIpAddr, uint16_t &aHcCtl, FrameBuilder &aFrameBuilder);
     Error CompressUdp(Message &aMessage, FrameBuilder &aFrameBuilder);
 
@@ -292,8 +287,7 @@
     Error DecompressUdpHeader(Message &aMessage, FrameData &aFrameData, uint16_t aDatagramLength);
     Error DispatchToNextHeader(uint8_t aDispatch, uint8_t &aNextHeader);
 
-    static void  CopyContext(const Context &aContext, Ip6::Address &aAddress);
-    static Error ComputeIid(const Mac::Address &aMacAddr, const Context &aContext, Ip6::Address &aIpAddress);
+    static Error ComputeIid(const Mac::Address &aMacAddr, const Context &aContext, Ip6::InterfaceIdentifier &aIid);
 };
 
 /**
@@ -304,12 +298,6 @@
 {
 public:
     /**
-     * The additional value that is added to predicted value of the route cost.
-     *
-     */
-    static constexpr uint8_t kAdditionalHopsLeft = 1;
-
-    /**
      * This method initializes the Mesh Header with a given Mesh Source, Mesh Destination and Hops Left value.
      *
      * @param[in]  aSource       The Mesh Source address.
@@ -428,16 +416,15 @@
     uint16_t GetDestination(void) const { return mDestination; }
 
     /**
-     * This method writes the Mesh Header into a given frame.
+     * This method appends the Mesh Header into a given frame.
      *
-     * @note This method expects the frame buffer to have enough space for the entire Mesh Header.
+     * @param[out]  aFrameBuilder  The `FrameBuilder` to append to.
      *
-     * @param[out]  aFrame  The pointer to the frame buffer to write to.
-     *
-     * @returns The header length (number of bytes written).
+     * @retval kErrorNone    Successfully appended the MeshHeader to @p aFrameBuilder.
+     * @retval kErrorNoBufs  Insufficient available buffers.
      *
      */
-    uint16_t WriteTo(uint8_t *aFrame) const;
+    Error AppendTo(FrameBuilder &aFrameBuilder) const;
 
     /**
      * This method appends the Mesh Header to a given message.
@@ -475,31 +462,70 @@
 class FragmentHeader
 {
 public:
-    static constexpr uint16_t kFirstFragmentHeaderSize      = 4; ///< First fragment header size in octets.
-    static constexpr uint16_t kSubsequentFragmentHeaderSize = 5; ///< Subsequent fragment header size in octets.
+    OT_TOOL_PACKED_BEGIN
+    class FirstFrag
+    {
+    public:
+        /**
+         * This method initializes the `FirstFrag`.
+         *
+         * @param[in] aSize  The Datagram Size value.
+         * @param[in] aTag   The Datagram Tag value.
+         *
+         */
+        void Init(uint16_t aSize, uint16_t aTag)
+        {
+            mDispatchSize = HostSwap16(kFirstDispatch | (aSize & kSizeMask));
+            mTag          = HostSwap16(aTag);
+        }
 
-    /**
-     * This method initializes the Fragment Header as a first fragment.
-     *
-     * A first fragment header starts at offset zero.
-     *
-     * @param[in] aSize   The Datagram Size value.
-     * @param[in] aTag    The Datagram Tag value.
-     *
-     */
-    void InitFirstFragment(uint16_t aSize, uint16_t aTag) { Init(aSize, aTag, 0); }
+    private:
+        //                       1                   2                   3
+        //   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+        //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        //  |1 1 0 0 0|    datagram_size    |         datagram_tag          |
+        //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 
-    /**
-     * This method initializes the Fragment Header.
-     *
-     * The @p aOffset value will be truncated to become a multiple of 8.
-     *
-     * @param[in] aSize   The Datagram Size value.
-     * @param[in] aTag    The Datagram Tag value.
-     * @param[in] aOffset The Datagram Offset value.
-     *
-     */
-    void Init(uint16_t aSize, uint16_t aTag, uint16_t aOffset);
+        static constexpr uint16_t kFirstDispatch = 0xc000; // 0b11000_0000_0000_0000
+
+        uint16_t mDispatchSize;
+        uint16_t mTag;
+    } OT_TOOL_PACKED_END;
+
+    OT_TOOL_PACKED_BEGIN
+    class NextFrag
+    {
+    public:
+        /**
+         * This method initializes the `NextFrag`.
+         *
+         * @param[in] aSize    The Datagram Size value.
+         * @param[in] aTag     The Datagram Tag value.
+         * @param[in] aOffset  The Datagram Offset value.
+         *
+         */
+        void Init(uint16_t aSize, uint16_t aTag, uint16_t aOffset)
+        {
+            mDispatchSize = HostSwap16(kNextDispatch | (aSize & kSizeMask));
+            mTag          = HostSwap16(aTag);
+            mOffset       = static_cast<uint8_t>(aOffset >> 3);
+        }
+
+    private:
+        //                       1                   2                   3
+        //   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+        //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        //  |1 1 1 0 0|    datagram_size    |         datagram_tag          |
+        //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        //  |datagram_offset|
+        //  +-+-+-+-+-+-+-+-+
+
+        static constexpr uint16_t kNextDispatch = 0xe000; // 0b11100_0000_0000_0000
+
+        uint16_t mDispatchSize;
+        uint16_t mTag;
+        uint8_t  mOffset;
+    } OT_TOOL_PACKED_END;
 
     /**
      * This static method indicates whether or not the header (in a given frame) is a Fragment Header.
@@ -517,19 +543,6 @@
     static bool IsFragmentHeader(const FrameData &aFrameData);
 
     /**
-     * This method parses the Fragment Header from a frame @p aFrame.
-     *
-     * @param[in]  aFrame          The pointer to the frame.
-     * @param[in]  aFrameLength    The length of the frame.
-     * @param[out] aHeaderLength   A reference to a variable to output the parsed header length (on success).
-     *
-     * @retval kErrorNone     Fragment Header parsed successfully.
-     * @retval kErrorParse    Fragment header could not be parsed from @p aFrame.
-     *
-     */
-    Error ParseFrom(const uint8_t *aFrame, uint16_t aFrameLength, uint16_t &aHeaderLength); //~~~ REMOVE OR MAKE PRIVATE
-
-    /**
      * This method parses the Fragment Header from a given frame data.
      *
      * If the Fragment Header is parsed successfully the @p aFrameData is updated to skip over the parsed header bytes.
@@ -581,18 +594,6 @@
      */
     uint16_t GetDatagramOffset(void) const { return mOffset; }
 
-    /**
-     * This method writes the Fragment Header into a given frame.
-     *
-     * @note This method expects the frame buffer to have enough space for the entire Fragment Header
-     *
-     * @param[out]  aFrame  The pointer to the frame buffer to write to.
-     *
-     * @returns The header length (number of bytes written).
-     *
-     */
-    uint16_t WriteTo(uint8_t *aFrame) const;
-
 private:
     static constexpr uint8_t kDispatch     = 0xc0;   // 0b1100_0000
     static constexpr uint8_t kDispatchMask = 0xd8;   // 0b1101_1000 accepts first (0b1100_0xxx) and next (0b1110_0xxx).
@@ -607,6 +608,8 @@
 
     static bool IsFragmentHeader(const uint8_t *aFrame, uint16_t aFrameLength);
 
+    Error ParseFrom(const uint8_t *aFrame, uint16_t aFrameLength, uint16_t &aHeaderLength);
+
     uint16_t mSize;
     uint16_t mTag;
     uint16_t mOffset;
diff --git a/src/core/thread/mesh_forwarder.cpp b/src/core/thread/mesh_forwarder.cpp
index bd55d74..a88830c 100644
--- a/src/core/thread/mesh_forwarder.cpp
+++ b/src/core/thread/mesh_forwarder.cpp
@@ -103,9 +103,9 @@
     , mSendBusy(false)
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
     , mDelayNextTx(false)
-    , mTxDelayTimer(aInstance, HandleTxDelayTimer)
+    , mTxDelayTimer(aInstance)
 #endif
-    , mScheduleTransmissionTask(aInstance, MeshForwarder::ScheduleTransmissionTask)
+    , mScheduleTransmissionTask(aInstance)
 #if OPENTHREAD_FTD
     , mIndirectSender(aInstance)
 #endif
@@ -164,51 +164,24 @@
 
 void MeshForwarder::PrepareEmptyFrame(Mac::TxFrame &aFrame, const Mac::Address &aMacDest, bool aAckRequest)
 {
-    uint16_t fcf       = 0;
-    bool     iePresent = CalcIePresent(nullptr);
+    Mac::Addresses addresses;
+    Mac::PanIds    panIds;
 
-    Mac::Address macSource;
-    macSource.SetShort(Get<Mac::Mac>().GetShortAddress());
+    addresses.mSource.SetShort(Get<Mac::Mac>().GetShortAddress());
 
-    if (macSource.IsShortAddrInvalid() || aMacDest.IsExtended())
+    if (addresses.mSource.IsShortAddrInvalid() || aMacDest.IsExtended())
     {
-        macSource.SetExtended(Get<Mac::Mac>().GetExtAddress());
+        addresses.mSource.SetExtended(Get<Mac::Mac>().GetExtAddress());
     }
 
-    fcf = Mac::Frame::kFcfFrameData | Mac::Frame::kFcfPanidCompression | Mac::Frame::kFcfSecurityEnabled;
+    addresses.mDestination = aMacDest;
+    panIds.mSource         = Get<Mac::Mac>().GetPanId();
+    panIds.mDestination    = Get<Mac::Mac>().GetPanId();
 
-    if (iePresent)
-    {
-        fcf |= Mac::Frame::kFcfIePresent;
-    }
+    PrepareMacHeaders(aFrame, Mac::Frame::kTypeData, addresses, panIds, Mac::Frame::kSecurityEncMic32,
+                      Mac::Frame::kKeyIdMode1, nullptr);
 
-    fcf |= CalcFrameVersion(Get<NeighborTable>().FindNeighbor(aMacDest), iePresent);
-
-    if (aAckRequest)
-    {
-        fcf |= Mac::Frame::kFcfAckRequest;
-    }
-
-    fcf |= (aMacDest.IsShort()) ? Mac::Frame::kFcfDstAddrShort : Mac::Frame::kFcfDstAddrExt;
-    fcf |= (macSource.IsShort()) ? Mac::Frame::kFcfSrcAddrShort : Mac::Frame::kFcfSrcAddrExt;
-
-    aFrame.InitMacHeader(fcf, Mac::Frame::kKeyIdMode1 | Mac::Frame::kSecEncMic32);
-
-    if (aFrame.IsDstPanIdPresent())
-    {
-        aFrame.SetDstPanId(Get<Mac::Mac>().GetPanId());
-    }
-    IgnoreError(aFrame.SetSrcPanId(Get<Mac::Mac>().GetPanId()));
-
-    aFrame.SetDstAddr(aMacDest);
-    aFrame.SetSrcAddr(macSource);
-    aFrame.SetFramePending(false);
-#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
-    if (iePresent)
-    {
-        AppendHeaderIe(nullptr, aFrame);
-    }
-#endif
+    aFrame.SetAckRequest(aAckRequest);
     aFrame.SetPayloadLength(0);
 }
 
@@ -247,11 +220,6 @@
 }
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
-void MeshForwarder::HandleTxDelayTimer(Timer &aTimer)
-{
-    aTimer.Get<MeshForwarder>().HandleTxDelayTimer();
-}
-
 void MeshForwarder::HandleTxDelayTimer(void)
 {
     mDelayNextTx = false;
@@ -521,11 +489,6 @@
 
 #endif // (OPENTHREAD_CONFIG_MAX_FRAMES_IN_DIRECT_TX_QUEUE > 0)
 
-void MeshForwarder::ScheduleTransmissionTask(Tasklet &aTasklet)
-{
-    aTasklet.Get<MeshForwarder>().ScheduleTransmissionTask();
-}
-
 void MeshForwarder::ScheduleTransmissionTask(void)
 {
     VerifyOrExit(!mSendBusy && !mTxPaused);
@@ -639,13 +602,13 @@
 
     VerifyOrExit(!ip6Header.GetSource().IsMulticast(), error = kErrorDrop);
 
-    GetMacSourceAddress(ip6Header.GetSource(), mMacSource);
+    GetMacSourceAddress(ip6Header.GetSource(), mMacAddrs.mSource);
 
     if (mle.IsDisabled() || mle.IsDetached())
     {
         if (ip6Header.GetDestination().IsLinkLocal() || ip6Header.GetDestination().IsLinkLocalMulticast())
         {
-            GetMacDestinationAddress(ip6Header.GetDestination(), mMacDest);
+            GetMacDestinationAddress(ip6Header.GetDestination(), mMacAddrs.mDestination);
         }
         else
         {
@@ -663,20 +626,20 @@
 
         if (mle.IsChild() && aMessage.IsLinkSecurityEnabled() && !aMessage.IsSubTypeMle())
         {
-            mMacDest.SetShort(mle.GetNextHop(Mac::kShortAddrBroadcast));
+            mMacAddrs.mDestination.SetShort(mle.GetNextHop(Mac::kShortAddrBroadcast));
         }
         else
         {
-            mMacDest.SetShort(Mac::kShortAddrBroadcast);
+            mMacAddrs.mDestination.SetShort(Mac::kShortAddrBroadcast);
         }
     }
     else if (ip6Header.GetDestination().IsLinkLocal())
     {
-        GetMacDestinationAddress(ip6Header.GetDestination(), mMacDest);
+        GetMacDestinationAddress(ip6Header.GetDestination(), mMacAddrs.mDestination);
     }
     else if (mle.IsMinimalEndDevice())
     {
-        mMacDest.SetShort(mle.GetNextHop(Mac::kShortAddrBroadcast));
+        mMacAddrs.mDestination.SetShort(mle.GetNextHop(Mac::kShortAddrBroadcast));
     }
     else
     {
@@ -691,10 +654,7 @@
     return error;
 }
 
-bool MeshForwarder::GetRxOnWhenIdle(void) const
-{
-    return Get<Mac::Mac>().GetRxOnWhenIdle();
-}
+bool MeshForwarder::GetRxOnWhenIdle(void) const { return Get<Mac::Mac>().GetRxOnWhenIdle(); }
 
 void MeshForwarder::SetRxOnWhenIdle(bool aRxOnWhenIdle)
 {
@@ -703,16 +663,12 @@
     if (aRxOnWhenIdle)
     {
         mDataPollSender.StopPolling();
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-        Get<Utils::SupervisionListener>().Stop();
-#endif
+        Get<SupervisionListener>().Stop();
     }
     else
     {
         mDataPollSender.StartPolling();
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-        Get<Utils::SupervisionListener>().Start();
-#endif
+        Get<SupervisionListener>().Start();
     }
 }
 
@@ -750,7 +706,7 @@
     VerifyOrExit(mEnabled && (mSendMessage != nullptr));
 
 #if OPENTHREAD_CONFIG_MULTI_RADIO
-    frame = &Get<RadioSelector>().SelectRadio(*mSendMessage, mMacDest, aTxFrames);
+    frame = &Get<RadioSelector>().SelectRadio(*mSendMessage, mMacAddrs.mDestination, aTxFrames);
 
     // If multi-radio link is supported, when sending frame with link
     // security enabled, Fragment Header is always included (even if
@@ -777,13 +733,13 @@
             VerifyOrExit(frame != nullptr);
         }
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-        if (Get<Mac::Mac>().IsCslEnabled() && mSendMessage->IsSubTypeMle())
+        else if (Get<Mac::Mac>().IsCslEnabled() && mSendMessage->IsSubTypeMle())
         {
             mSendMessage->SetLinkSecurityEnabled(true);
         }
 #endif
-        mMessageNextOffset = PrepareDataFrame(*frame, *mSendMessage, mMacSource, mMacDest, mAddMeshHeader, mMeshSource,
-                                              mMeshDest, addFragHeader);
+        mMessageNextOffset =
+            PrepareDataFrame(*frame, *mSendMessage, mMacAddrs, mAddMeshHeader, mMeshSource, mMeshDest, addFragHeader);
 
         if ((mSendMessage->GetSubType() == Message::kSubTypeMleChildIdRequest) && mSendMessage->IsLinkSecurityEnabled())
         {
@@ -832,6 +788,30 @@
     return frame;
 }
 
+void MeshForwarder::PrepareMacHeaders(Mac::TxFrame             &aFrame,
+                                      Mac::Frame::Type          aFrameType,
+                                      const Mac::Addresses     &aMacAddrs,
+                                      const Mac::PanIds        &aPanIds,
+                                      Mac::Frame::SecurityLevel aSecurityLevel,
+                                      Mac::Frame::KeyIdMode     aKeyIdMode,
+                                      const Message            *aMessage)
+{
+    bool                iePresent;
+    Mac::Frame::Version version;
+
+    iePresent = CalcIePresent(aMessage);
+    version   = CalcFrameVersion(Get<NeighborTable>().FindNeighbor(aMacAddrs.mDestination), iePresent);
+
+    aFrame.InitMacHeader(aFrameType, version, aMacAddrs, aPanIds, aSecurityLevel, aKeyIdMode);
+
+#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
+    if (iePresent)
+    {
+        AppendHeaderIe(aMessage, aFrame);
+    }
+#endif
+}
+
 // This method constructs a MAC data from from a given IPv6 message.
 //
 // This method handles generation of MAC header, mesh header (if
@@ -843,151 +823,78 @@
 // It returns the next offset into the message after the prepared
 // frame.
 //
-uint16_t MeshForwarder::PrepareDataFrame(Mac::TxFrame &      aFrame,
-                                         Message &           aMessage,
-                                         const Mac::Address &aMacSource,
-                                         const Mac::Address &aMacDest,
-                                         bool                aAddMeshHeader,
-                                         uint16_t            aMeshSource,
-                                         uint16_t            aMeshDest,
-                                         bool                aAddFragHeader)
+uint16_t MeshForwarder::PrepareDataFrame(Mac::TxFrame         &aFrame,
+                                         Message              &aMessage,
+                                         const Mac::Addresses &aMacAddrs,
+                                         bool                  aAddMeshHeader,
+                                         uint16_t              aMeshSource,
+                                         uint16_t              aMeshDest,
+                                         bool                  aAddFragHeader)
 {
-    uint16_t fcf;
-    uint8_t *payload;
-    uint8_t  headerLength;
-    uint16_t maxPayloadLength;
-    uint16_t payloadLength;
-    uint16_t fragmentLength;
-    uint16_t dstpan;
-    uint8_t  secCtl;
-    uint16_t nextOffset;
-    bool     iePresent = CalcIePresent(&aMessage);
+    Mac::Frame::SecurityLevel securityLevel;
+    Mac::Frame::KeyIdMode     keyIdMode;
+    Mac::PanIds               panIds;
+    uint16_t                  payloadLength;
+    uint16_t                  origMsgOffset;
+    uint16_t                  nextOffset;
+    FrameBuilder              frameBuilder;
 
 start:
 
-    // Initialize MAC header
-    fcf = Mac::Frame::kFcfFrameData;
-
-    fcf |= (aMacDest.IsShort()) ? Mac::Frame::kFcfDstAddrShort : Mac::Frame::kFcfDstAddrExt;
-    fcf |= (aMacSource.IsShort()) ? Mac::Frame::kFcfSrcAddrShort : Mac::Frame::kFcfSrcAddrExt;
-
-    if (iePresent)
-    {
-        fcf |= Mac::Frame::kFcfIePresent;
-    }
-
-    fcf |= CalcFrameVersion(Get<NeighborTable>().FindNeighbor(aMacDest), iePresent);
-
-    // All unicast frames request ACK
-    if (aMacDest.IsExtended() || !aMacDest.IsBroadcast())
-    {
-        fcf |= Mac::Frame::kFcfAckRequest;
-    }
+    securityLevel = Mac::Frame::kSecurityNone;
+    keyIdMode     = Mac::Frame::kKeyIdMode1;
 
     if (aMessage.IsLinkSecurityEnabled())
     {
-        fcf |= Mac::Frame::kFcfSecurityEnabled;
+        securityLevel = Mac::Frame::kSecurityEncMic32;
 
         switch (aMessage.GetSubType())
         {
         case Message::kSubTypeJoinerEntrust:
-            secCtl = static_cast<uint8_t>(Mac::Frame::kKeyIdMode0);
+            keyIdMode = Mac::Frame::kKeyIdMode0;
             break;
 
         case Message::kSubTypeMleAnnounce:
-            secCtl = static_cast<uint8_t>(Mac::Frame::kKeyIdMode2);
+            keyIdMode = Mac::Frame::kKeyIdMode2;
             break;
 
         default:
-            secCtl = static_cast<uint8_t>(Mac::Frame::kKeyIdMode1);
+            // Use the `kKeyIdMode1`
             break;
         }
-
-        secCtl |= Mac::Frame::kSecEncMic32;
-    }
-    else
-    {
-        secCtl = Mac::Frame::kSecNone;
     }
 
-    dstpan = Get<Mac::Mac>().GetPanId();
+    panIds.mSource      = Get<Mac::Mac>().GetPanId();
+    panIds.mDestination = Get<Mac::Mac>().GetPanId();
 
     switch (aMessage.GetSubType())
     {
     case Message::kSubTypeMleAnnounce:
         aFrame.SetChannel(aMessage.GetChannel());
-        dstpan = Mac::kPanIdBroadcast;
+        aFrame.SetRxChannelAfterTxDone(Get<Mac::Mac>().GetPanChannel());
+        panIds.mDestination = Mac::kPanIdBroadcast;
         break;
 
     case Message::kSubTypeMleDiscoverRequest:
     case Message::kSubTypeMleDiscoverResponse:
-        dstpan = aMessage.GetPanId();
+        panIds.mDestination = aMessage.GetPanId();
         break;
 
     default:
         break;
     }
 
-    // Handle special case in 15.4-2015:
-    //  Dest Address: Extended
-    //  Source Address: Extended
-    //  Dest PanId: Present
-    //  Src Panid: Not Present
-    //  Pan ID Compression: 0
-    if (dstpan == Get<Mac::Mac>().GetPanId() &&
-        ((fcf & Mac::Frame::kFcfFrameVersionMask) == Mac::Frame::kFcfFrameVersion2006 ||
-         (fcf & Mac::Frame::kFcfDstAddrMask) != Mac::Frame::kFcfDstAddrExt ||
-         (fcf & Mac::Frame::kFcfSrcAddrMask) != Mac::Frame::kFcfSrcAddrExt))
-    {
-#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
-        // Handle a special case in IEEE 802.15.4-2015, when Pan ID Compression is 0, but Src Pan ID is not present:
-        //  Dest Address:       Extended
-        //  Src Address:        Extended
-        //  Dest Pan ID:        Present
-        //  Src Pan ID:         Not Present
-        //  Pan ID Compression: 0
+    PrepareMacHeaders(aFrame, Mac::Frame::kTypeData, aMacAddrs, panIds, securityLevel, keyIdMode, &aMessage);
 
-        if ((fcf & Mac::Frame::kFcfFrameVersionMask) != Mac::Frame::kFcfFrameVersion2015 ||
-            (fcf & Mac::Frame::kFcfDstAddrMask) != Mac::Frame::kFcfDstAddrExt ||
-            (fcf & Mac::Frame::kFcfSrcAddrMask) != Mac::Frame::kFcfSrcAddrExt)
-#endif
-        {
-            fcf |= Mac::Frame::kFcfPanidCompression;
-        }
-    }
-
-    aFrame.InitMacHeader(fcf, secCtl);
-
-    if (aFrame.IsDstPanIdPresent())
-    {
-        aFrame.SetDstPanId(dstpan);
-    }
-
-    IgnoreError(aFrame.SetSrcPanId(Get<Mac::Mac>().GetPanId()));
-    aFrame.SetDstAddr(aMacDest);
-    aFrame.SetSrcAddr(aMacSource);
-
-#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
-    if (iePresent)
-    {
-        AppendHeaderIe(&aMessage, aFrame);
-    }
-#endif
-
-    payload          = aFrame.GetPayload();
-    maxPayloadLength = aFrame.GetMaxPayloadLength();
-
-    headerLength = 0;
+    frameBuilder.Init(aFrame.GetPayload(), aFrame.GetMaxPayloadLength());
 
 #if OPENTHREAD_FTD
 
     // Initialize Mesh header
     if (aAddMeshHeader)
     {
-        Mle::MleRouter &   mle = Get<Mle::MleRouter>();
         Lowpan::MeshHeader meshHeader;
-        uint16_t           meshHeaderLength;
-        uint8_t            hopsLeft;
+        uint16_t           maxPayloadLength;
 
         // Mesh Header frames are forwarded by routers over multiple
         // hops to reach a final destination. The forwarding path can
@@ -1007,82 +914,72 @@
         maxPayloadLength = kMeshHeaderFrameMtu - aFrame.GetHeaderLength() -
                            (aFrame.GetFooterLength() - aFrame.GetFcsSize() + kMeshHeaderFrameFcsSize);
 
-        if (mle.IsChild())
-        {
-            // REED sets hopsLeft to max (16) + 1. It does not know the route cost.
-            hopsLeft = Mle::kMaxRouteCost + 1;
-        }
-        else
-        {
-            // Calculate the number of predicted hops.
-            hopsLeft = mle.GetRouteCost(aMeshDest);
+        frameBuilder.Init(aFrame.GetPayload(), maxPayloadLength);
 
-            if (hopsLeft != Mle::kMaxRouteCost)
-            {
-                hopsLeft += mle.GetLinkCost(Mle::Mle::RouterIdFromRloc16(mle.GetNextHop(aMeshDest)));
-            }
-            else
-            {
-                // In case there is no route to the destination router (only link).
-                hopsLeft = mle.GetLinkCost(Mle::Mle::RouterIdFromRloc16(aMeshDest));
-            }
-        }
+        meshHeader.Init(aMeshSource, aMeshDest, kMeshHeaderHopsLeft);
 
-        // The hopsLft field MUST be incremented by one if the
-        // destination RLOC16 is not that of an active Router.
-        if (!Mle::Mle::IsActiveRouter(aMeshDest))
-        {
-            hopsLeft += 1;
-        }
-
-        meshHeader.Init(aMeshSource, aMeshDest, hopsLeft + Lowpan::MeshHeader::kAdditionalHopsLeft);
-        meshHeaderLength = meshHeader.WriteTo(payload);
-        payload += meshHeaderLength;
-        headerLength += meshHeaderLength;
+        IgnoreError(meshHeader.AppendTo(frameBuilder));
     }
 
-#endif
+#endif // OPENTHREAD_FTD
+
+    // While performing lowpan compression, the message offset may be
+    // changed to skip over the compressed IPv6 headers, we save the
+    // original offset and set it back on `aMessage` at the end
+    // before returning.
+
+    origMsgOffset = aMessage.GetOffset();
 
     // Compress IPv6 Header
     if (aMessage.GetOffset() == 0)
     {
-        FrameBuilder frameBuilder;
-        uint8_t      hcLength;
-        Mac::Address meshSource, meshDest;
+        uint16_t       fragHeaderOffset;
+        uint16_t       maxFrameLength;
+        Mac::Addresses macAddrs;
 
-        frameBuilder.Init(payload, maxPayloadLength - headerLength - Lowpan::FragmentHeader::kFirstFragmentHeaderSize);
+        // Before performing lowpan header compression, we reduce the
+        // max length on `frameBuilder` to reserve bytes for first
+        // fragment header. This ensures that lowpan compression will
+        // leave room for a first fragment header. After the lowpan
+        // header compression is done, we reclaim the reserved bytes
+        // by setting the max length back to its original value.
+
+        fragHeaderOffset = frameBuilder.GetLength();
+        maxFrameLength   = frameBuilder.GetMaxLength();
+        frameBuilder.SetMaxLength(maxFrameLength - sizeof(Lowpan::FragmentHeader::FirstFrag));
 
         if (aAddMeshHeader)
         {
-            meshSource.SetShort(aMeshSource);
-            meshDest.SetShort(aMeshDest);
+            macAddrs.mSource.SetShort(aMeshSource);
+            macAddrs.mDestination.SetShort(aMeshDest);
         }
         else
         {
-            meshSource = aMacSource;
-            meshDest   = aMacDest;
+            macAddrs = aMacAddrs;
         }
 
-        SuccessOrAssert(Get<Lowpan::Lowpan>().Compress(aMessage, meshSource, meshDest, frameBuilder));
+        SuccessOrAssert(Get<Lowpan::Lowpan>().Compress(aMessage, macAddrs, frameBuilder));
 
-        hcLength = static_cast<uint8_t>(frameBuilder.GetLength());
-        headerLength += hcLength;
-        payloadLength  = aMessage.GetLength() - aMessage.GetOffset();
-        fragmentLength = maxPayloadLength - headerLength;
+        frameBuilder.SetMaxLength(maxFrameLength);
 
-        if ((payloadLength > fragmentLength) || aAddFragHeader)
+        payloadLength = aMessage.GetLength() - aMessage.GetOffset();
+
+        if (aAddFragHeader || (payloadLength > frameBuilder.GetRemainingLength()))
         {
-            Lowpan::FragmentHeader fragmentHeader;
+            Lowpan::FragmentHeader::FirstFrag firstFragHeader;
 
             if ((!aMessage.IsLinkSecurityEnabled()) && aMessage.IsSubTypeMle())
             {
-                // Enable security and try again.
+                // MLE messages that require fragmentation MUST use
+                // link-layer security. We enable security and try
+                // constructing the frame again.
+
                 aMessage.SetOffset(0);
                 aMessage.SetLinkSecurityEnabled(true);
                 goto start;
             }
 
-            // Write Fragment header
+            // Insert Fragment header
             if (aMessage.GetDatagramTag() == 0)
             {
                 // Avoid using datagram tag value 0, which indicates the tag has not been set
@@ -1094,60 +991,32 @@
                 aMessage.SetDatagramTag(mFragTag++);
             }
 
-            memmove(payload + Lowpan::FragmentHeader::kFirstFragmentHeaderSize, payload, hcLength);
-
-            fragmentHeader.InitFirstFragment(aMessage.GetLength(), static_cast<uint16_t>(aMessage.GetDatagramTag()));
-            fragmentHeader.WriteTo(payload);
-
-            payload += Lowpan::FragmentHeader::kFirstFragmentHeaderSize;
-            headerLength += Lowpan::FragmentHeader::kFirstFragmentHeaderSize;
-
-            fragmentLength = maxPayloadLength - headerLength;
-
-            if (payloadLength > fragmentLength)
-            {
-                payloadLength = fragmentLength & ~0x7;
-            }
+            firstFragHeader.Init(aMessage.GetLength(), static_cast<uint16_t>(aMessage.GetDatagramTag()));
+            SuccessOrAssert(frameBuilder.Insert(fragHeaderOffset, firstFragHeader));
         }
-
-        payload += hcLength;
-
-        // copy IPv6 Payload
-        aMessage.ReadBytes(aMessage.GetOffset(), payload, payloadLength);
-        aFrame.SetPayloadLength(headerLength + payloadLength);
-
-        nextOffset = aMessage.GetOffset() + payloadLength;
-        aMessage.SetOffset(0);
     }
     else
     {
-        Lowpan::FragmentHeader fragmentHeader;
-        uint16_t               fragmentHeaderLength;
+        Lowpan::FragmentHeader::NextFrag nextFragHeader;
+
+        nextFragHeader.Init(aMessage.GetLength(), static_cast<uint16_t>(aMessage.GetDatagramTag()),
+                            aMessage.GetOffset());
+        SuccessOrAssert(frameBuilder.Append(nextFragHeader));
 
         payloadLength = aMessage.GetLength() - aMessage.GetOffset();
-
-        // Write Fragment header
-        fragmentHeader.Init(aMessage.GetLength(), static_cast<uint16_t>(aMessage.GetDatagramTag()),
-                            aMessage.GetOffset());
-        fragmentHeaderLength = fragmentHeader.WriteTo(payload);
-
-        payload += fragmentHeaderLength;
-        headerLength += fragmentHeaderLength;
-
-        fragmentLength = maxPayloadLength - headerLength;
-
-        if (payloadLength > fragmentLength)
-        {
-            payloadLength = (fragmentLength & ~0x7);
-        }
-
-        // Copy IPv6 Payload
-        aMessage.ReadBytes(aMessage.GetOffset(), payload, payloadLength);
-        aFrame.SetPayloadLength(headerLength + payloadLength);
-
-        nextOffset = aMessage.GetOffset() + payloadLength;
     }
 
+    if (payloadLength > frameBuilder.GetRemainingLength())
+    {
+        payloadLength = (frameBuilder.GetRemainingLength() & ~0x7);
+    }
+
+    // Copy IPv6 Payload
+    SuccessOrAssert(frameBuilder.AppendBytesFromMessage(aMessage, aMessage.GetOffset(), payloadLength));
+    aFrame.SetPayloadLength(frameBuilder.GetLength());
+
+    nextOffset = aMessage.GetOffset() + payloadLength;
+
     if (nextOffset < aMessage.GetLength())
     {
         aFrame.SetFramePending(true);
@@ -1156,10 +1025,12 @@
 #endif
     }
 
+    aMessage.SetOffset(origMsgOffset);
+
     return nextOffset;
 }
 
-Neighbor *MeshForwarder::UpdateNeighborOnSentFrame(Mac::TxFrame &      aFrame,
+Neighbor *MeshForwarder::UpdateNeighborOnSentFrame(Mac::TxFrame       &aFrame,
                                                    Error               aError,
                                                    const Mac::Address &aMacDest,
                                                    bool                aIsDataPoll)
@@ -1218,7 +1089,7 @@
     {
         aNeighbor.IncrementLinkFailures();
 
-        if (aAllowNeighborRemove && (Mle::Mle::IsActiveRouter(aNeighbor.GetRloc16())) &&
+        if (aAllowNeighborRemove && (Mle::IsActiveRouter(aNeighbor.GetRloc16())) &&
             (aNeighbor.GetLinkFailures() >= aFailLimit))
         {
             Get<Mle::MleRouter>().RemoveRouterLink(static_cast<Router &>(aNeighbor));
@@ -1254,7 +1125,7 @@
 
 void MeshForwarder::HandleSentFrame(Mac::TxFrame &aFrame, Error aError)
 {
-    Neighbor *   neighbor = nullptr;
+    Neighbor    *neighbor = nullptr;
     Mac::Address macDest;
 
     OT_ASSERT((aError == kErrorNone) || (aError == kErrorChannelAccessFailure) || (aError == kErrorAbort) ||
@@ -1268,7 +1139,7 @@
     if (mDelayNextTx && (aError == kErrorNone))
     {
         mTxDelayTimer.Start(kTxDelayInterval);
-        LogDebg("Start tx delay timer for %u msec", kTxDelayInterval);
+        LogDebg("Start tx delay timer for %lu msec", ToUlong(kTxDelayInterval));
     }
     else
     {
@@ -1414,40 +1285,37 @@
 void MeshForwarder::HandleReceivedFrame(Mac::RxFrame &aFrame)
 {
     ThreadLinkInfo linkInfo;
-    Mac::Address   macDest;
-    Mac::Address   macSource;
+    Mac::Addresses macAddrs;
     FrameData      frameData;
     Error          error = kErrorNone;
 
     VerifyOrExit(mEnabled, error = kErrorInvalidState);
 
-    SuccessOrExit(error = aFrame.GetSrcAddr(macSource));
-    SuccessOrExit(error = aFrame.GetDstAddr(macDest));
+    SuccessOrExit(error = aFrame.GetSrcAddr(macAddrs.mSource));
+    SuccessOrExit(error = aFrame.GetDstAddr(macAddrs.mDestination));
 
     linkInfo.SetFrom(aFrame);
 
     frameData.Init(aFrame.GetPayload(), aFrame.GetPayloadLength());
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-    Get<Utils::SupervisionListener>().UpdateOnReceive(macSource, linkInfo.IsLinkSecurityEnabled());
-#endif
+    Get<SupervisionListener>().UpdateOnReceive(macAddrs.mSource, linkInfo.IsLinkSecurityEnabled());
 
     switch (aFrame.GetType())
     {
-    case Mac::Frame::kFcfFrameData:
+    case Mac::Frame::kTypeData:
         if (Lowpan::MeshHeader::IsMeshHeader(frameData))
         {
 #if OPENTHREAD_FTD
-            HandleMesh(frameData, macSource, linkInfo);
+            HandleMesh(frameData, macAddrs.mSource, linkInfo);
 #endif
         }
         else if (Lowpan::FragmentHeader::IsFragmentHeader(frameData))
         {
-            HandleFragment(frameData, macSource, macDest, linkInfo);
+            HandleFragment(frameData, macAddrs, linkInfo);
         }
         else if (Lowpan::Lowpan::IsLowpanHc(frameData))
         {
-            HandleLowpanHC(frameData, macSource, macDest, linkInfo);
+            HandleLowpanHC(frameData, macAddrs, linkInfo);
         }
         else
         {
@@ -1458,7 +1326,7 @@
 
         break;
 
-    case Mac::Frame::kFcfFrameBeacon:
+    case Mac::Frame::kTypeBeacon:
         break;
 
     default:
@@ -1474,14 +1342,13 @@
     }
 }
 
-void MeshForwarder::HandleFragment(FrameData &           aFrameData,
-                                   const Mac::Address &  aMacSource,
-                                   const Mac::Address &  aMacDest,
+void MeshForwarder::HandleFragment(FrameData            &aFrameData,
+                                   const Mac::Addresses &aMacAddrs,
                                    const ThreadLinkInfo &aLinkInfo)
 {
     Error                  error = kErrorNone;
     Lowpan::FragmentHeader fragmentHeader;
-    Message *              message = nullptr;
+    Message               *message = nullptr;
 
     SuccessOrExit(error = fragmentHeader.ParseFrom(aFrameData));
 
@@ -1489,7 +1356,7 @@
 
     if (aLinkInfo.mLinkSecurity)
     {
-        Neighbor *neighbor = Get<NeighborTable>().FindNeighbor(aMacSource, Neighbor::kInStateAnyExceptInvalid);
+        Neighbor *neighbor = Get<NeighborTable>().FindNeighbor(aMacAddrs.mSource, Neighbor::kInStateAnyExceptInvalid);
 
         if (neighbor != nullptr)
         {
@@ -1525,10 +1392,10 @@
         uint16_t datagramSize = fragmentHeader.GetDatagramSize();
 
 #if OPENTHREAD_FTD
-        UpdateRoutes(aFrameData, aMacSource, aMacDest);
+        UpdateRoutes(aFrameData, aMacAddrs);
 #endif
 
-        SuccessOrExit(error = FrameToMessage(aFrameData, datagramSize, aMacSource, aMacDest, message));
+        SuccessOrExit(error = FrameToMessage(aFrameData, datagramSize, aMacAddrs, message));
 
         VerifyOrExit(datagramSize >= message->GetLength(), error = kErrorParse);
         SuccessOrExit(error = message->SetLength(datagramSize));
@@ -1540,7 +1407,7 @@
         VerifyOrExit(Get<Ip6::Filter>().Accept(*message), error = kErrorDrop);
 
 #if OPENTHREAD_FTD
-        SendIcmpErrorIfDstUnreach(*message, aMacSource, aMacDest);
+        SendIcmpErrorIfDstUnreach(*message, aMacAddrs);
 #endif
 
         // Allow re-assembly of only one message at a time on a SED by clearing
@@ -1601,12 +1468,12 @@
         if (message->GetOffset() >= message->GetLength())
         {
             mReassemblyList.Dequeue(*message);
-            IgnoreError(HandleDatagram(*message, aLinkInfo, aMacSource));
+            IgnoreError(HandleDatagram(*message, aLinkInfo, aMacAddrs.mSource));
         }
     }
     else
     {
-        LogFragmentFrameDrop(error, aFrameData.GetLength(), aMacSource, aMacDest, fragmentHeader,
+        LogFragmentFrameDrop(error, aFrameData.GetLength(), aMacAddrs, fragmentHeader,
                              aLinkInfo.IsLinkSecurityEnabled());
         FreeMessage(message);
     }
@@ -1629,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);
     }
@@ -1665,22 +1532,21 @@
     return mReassemblyList.GetHead() != nullptr;
 }
 
-Error MeshForwarder::FrameToMessage(const FrameData &   aFrameData,
-                                    uint16_t            aDatagramSize,
-                                    const Mac::Address &aMacSource,
-                                    const Mac::Address &aMacDest,
-                                    Message *&          aMessage)
+Error MeshForwarder::FrameToMessage(const FrameData      &aFrameData,
+                                    uint16_t              aDatagramSize,
+                                    const Mac::Addresses &aMacAddrs,
+                                    Message             *&aMessage)
 {
     Error             error     = kErrorNone;
     FrameData         frameData = aFrameData;
     Message::Priority priority;
 
-    SuccessOrExit(error = GetFramePriority(frameData, aMacSource, aMacDest, priority));
+    SuccessOrExit(error = GetFramePriority(frameData, aMacAddrs, priority));
 
     aMessage = Get<MessagePool>().Allocate(Message::kTypeIp6, /* aReserveHeader */ 0, Message::Settings(priority));
     VerifyOrExit(aMessage, error = kErrorNoBufs);
 
-    SuccessOrExit(error = Get<Lowpan::Lowpan>().Decompress(*aMessage, aMacSource, aMacDest, frameData, aDatagramSize));
+    SuccessOrExit(error = Get<Lowpan::Lowpan>().Decompress(*aMessage, aMacAddrs, frameData, aDatagramSize));
 
     SuccessOrExit(error = aMessage->AppendData(frameData));
     aMessage->MoveOffset(frameData.GetLength());
@@ -1689,45 +1555,42 @@
     return error;
 }
 
-void MeshForwarder::HandleLowpanHC(const FrameData &     aFrameData,
-                                   const Mac::Address &  aMacSource,
-                                   const Mac::Address &  aMacDest,
+void MeshForwarder::HandleLowpanHC(const FrameData      &aFrameData,
+                                   const Mac::Addresses &aMacAddrs,
                                    const ThreadLinkInfo &aLinkInfo)
 {
     Error    error   = kErrorNone;
     Message *message = nullptr;
 
 #if OPENTHREAD_FTD
-    UpdateRoutes(aFrameData, aMacSource, aMacDest);
+    UpdateRoutes(aFrameData, aMacAddrs);
 #endif
 
-    SuccessOrExit(error = FrameToMessage(aFrameData, 0, aMacSource, aMacDest, message));
+    SuccessOrExit(error = FrameToMessage(aFrameData, 0, aMacAddrs, message));
 
     message->SetLinkInfo(aLinkInfo);
 
     VerifyOrExit(Get<Ip6::Filter>().Accept(*message), error = kErrorDrop);
 
 #if OPENTHREAD_FTD
-    SendIcmpErrorIfDstUnreach(*message, aMacSource, aMacDest);
+    SendIcmpErrorIfDstUnreach(*message, aMacAddrs);
 #endif
 
 exit:
 
     if (error == kErrorNone)
     {
-        IgnoreError(HandleDatagram(*message, aLinkInfo, aMacSource));
+        IgnoreError(HandleDatagram(*message, aLinkInfo, aMacAddrs.mSource));
     }
     else
     {
-        LogLowpanHcFrameDrop(error, aFrameData.GetLength(), aMacSource, aMacDest, aLinkInfo.IsLinkSecurityEnabled());
+        LogLowpanHcFrameDrop(error, aFrameData.GetLength(), aMacAddrs, aLinkInfo.IsLinkSecurityEnabled());
         FreeMessage(message);
     }
 }
 
 Error MeshForwarder::HandleDatagram(Message &aMessage, const ThreadLinkInfo &aLinkInfo, const Mac::Address &aMacSource)
 {
-    ThreadNetif &netif = Get<ThreadNetif>();
-
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
     Get<Utils::HistoryTracker>().RecordRxMessage(aMessage, aMacSource);
 #endif
@@ -1739,18 +1602,17 @@
         mIpCounters.mRxSuccess++;
     }
 
-    return Get<Ip6::Ip6>().HandleDatagram(aMessage, &netif, &aLinkInfo, false);
+    return Get<Ip6::Ip6>().HandleDatagram(aMessage, Ip6::Ip6::kFromThreadNetif, &aLinkInfo);
 }
 
-Error MeshForwarder::GetFramePriority(const FrameData &   aFrameData,
-                                      const Mac::Address &aMacSource,
-                                      const Mac::Address &aMacDest,
-                                      Message::Priority & aPriority)
+Error MeshForwarder::GetFramePriority(const FrameData      &aFrameData,
+                                      const Mac::Addresses &aMacAddrs,
+                                      Message::Priority    &aPriority)
 {
     Error        error = kErrorNone;
     Ip6::Headers headers;
 
-    SuccessOrExit(error = headers.DecompressFrom(aFrameData, aMacSource, aMacDest, GetInstance()));
+    SuccessOrExit(error = headers.DecompressFrom(aFrameData, aMacAddrs, GetInstance()));
 
     aPriority = Ip6::Ip6::DscpToPriority(headers.GetIp6Header().GetDscp());
 
@@ -1764,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:
@@ -1807,7 +1673,10 @@
     iePresent |= (aMessage != nullptr && aMessage->IsTimeSync());
 #endif
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    iePresent |= Get<Mac::Mac>().IsCslEnabled();
+    if (!(aMessage != nullptr && aMessage->GetSubType() == Message::kSubTypeMleDiscoverRequest))
+    {
+        iePresent |= Get<Mac::Mac>().IsCslEnabled();
+    }
 #endif
 #endif
 
@@ -1820,7 +1689,7 @@
     uint8_t index     = 0;
     bool    iePresent = false;
     bool    payloadPresent =
-        (aFrame.GetType() == Mac::Frame::kFcfFrameMacCmd) || (aMessage != nullptr && aMessage->GetLength() != 0);
+        (aFrame.GetType() == Mac::Frame::kTypeMacCmd) || (aMessage != nullptr && aMessage->GetLength() != 0);
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
     if (aMessage != nullptr && aMessage->IsTimeSync())
@@ -1850,26 +1719,26 @@
 }
 #endif
 
-uint16_t MeshForwarder::CalcFrameVersion(const Neighbor *aNeighbor, bool aIePresent)
+Mac::Frame::Version MeshForwarder::CalcFrameVersion(const Neighbor *aNeighbor, bool aIePresent) const
 {
-    uint16_t version = Mac::Frame::kFcfFrameVersion2006;
+    Mac::Frame::Version version = Mac::Frame::kVersion2006;
     OT_UNUSED_VARIABLE(aNeighbor);
 
     if (aIePresent)
     {
-        version = Mac::Frame::kFcfFrameVersion2015;
+        version = Mac::Frame::kVersion2015;
     }
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
-    else if (aNeighbor != nullptr && !Mle::MleRouter::IsActiveRouter(aNeighbor->GetRloc16()) &&
+    else if ((aNeighbor != nullptr) && Get<ChildTable>().Contains(*aNeighbor) &&
              static_cast<const Child *>(aNeighbor)->IsCslSynchronized())
     {
-        version = Mac::Frame::kFcfFrameVersion2015;
+        version = Mac::Frame::kVersion2015;
     }
 #endif
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
     else if (aNeighbor != nullptr && aNeighbor->IsEnhAckProbingActive())
     {
-        version = Mac::Frame::kFcfFrameVersion2015; ///< Set version to 2015 to fetch Link Metrics data in Enh-ACK.
+        version = Mac::Frame::kVersion2015; ///< Set version to 2015 to fetch Link Metrics data in Enh-ACK.
     }
 #endif
 
@@ -1956,13 +1825,11 @@
     }
 }
 #else
-void MeshForwarder::LogIp6SourceDestAddresses(const Ip6::Headers &, LogLevel)
-{
-}
+void MeshForwarder::LogIp6SourceDestAddresses(const Ip6::Headers &, LogLevel) {}
 #endif
 
 void MeshForwarder::LogIp6Message(MessageAction       aAction,
-                                  const Message &     aMessage,
+                                  const Message      &aMessage,
                                   const Mac::Address *aMacAddress,
                                   Error               aError,
                                   LogLevel            aLogLevel)
@@ -1970,7 +1837,7 @@
     Ip6::Headers headers;
     bool         shouldLogRss;
     bool         shouldLogRadio = false;
-    const char * radioString    = "";
+    const char  *radioString    = "";
 
     SuccessOrExit(headers.ParseFrom(aMessage));
 
@@ -2002,7 +1869,7 @@
 }
 
 void MeshForwarder::LogMessage(MessageAction       aAction,
-                               const Message &     aMessage,
+                               const Message      &aMessage,
                                Error               aError,
                                const Mac::Address *aMacAddress)
 
@@ -2069,49 +1936,37 @@
 
 void MeshForwarder::LogFragmentFrameDrop(Error                         aError,
                                          uint16_t                      aFrameLength,
-                                         const Mac::Address &          aMacSource,
-                                         const Mac::Address &          aMacDest,
+                                         const Mac::Addresses         &aMacAddrs,
                                          const Lowpan::FragmentHeader &aFragmentHeader,
                                          bool                          aIsSecure)
 {
     LogNote("Dropping rx frag frame, error:%s, len:%d, src:%s, dst:%s, tag:%d, offset:%d, dglen:%d, sec:%s",
-            ErrorToString(aError), aFrameLength, aMacSource.ToString().AsCString(), aMacDest.ToString().AsCString(),
-            aFragmentHeader.GetDatagramTag(), aFragmentHeader.GetDatagramOffset(), aFragmentHeader.GetDatagramSize(),
-            ToYesNo(aIsSecure));
+            ErrorToString(aError), aFrameLength, aMacAddrs.mSource.ToString().AsCString(),
+            aMacAddrs.mDestination.ToString().AsCString(), aFragmentHeader.GetDatagramTag(),
+            aFragmentHeader.GetDatagramOffset(), aFragmentHeader.GetDatagramSize(), ToYesNo(aIsSecure));
 }
 
-void MeshForwarder::LogLowpanHcFrameDrop(Error               aError,
-                                         uint16_t            aFrameLength,
-                                         const Mac::Address &aMacSource,
-                                         const Mac::Address &aMacDest,
-                                         bool                aIsSecure)
+void MeshForwarder::LogLowpanHcFrameDrop(Error                 aError,
+                                         uint16_t              aFrameLength,
+                                         const Mac::Addresses &aMacAddrs,
+                                         bool                  aIsSecure)
 {
     LogNote("Dropping rx lowpan HC frame, error:%s, len:%d, src:%s, dst:%s, sec:%s", ErrorToString(aError),
-            aFrameLength, aMacSource.ToString().AsCString(), aMacDest.ToString().AsCString(), ToYesNo(aIsSecure));
+            aFrameLength, aMacAddrs.mSource.ToString().AsCString(), aMacAddrs.mDestination.ToString().AsCString(),
+            ToYesNo(aIsSecure));
 }
 
 #else // #if OT_SHOULD_LOG_AT( OT_LOG_LEVEL_NOTE)
 
-void MeshForwarder::LogMessage(MessageAction, const Message &, Error, const Mac::Address *)
+void MeshForwarder::LogMessage(MessageAction, const Message &, Error, const Mac::Address *) {}
+
+void MeshForwarder::LogFrame(const char *, const Mac::Frame &, Error) {}
+
+void MeshForwarder::LogFragmentFrameDrop(Error, uint16_t, const Mac::Addresses &, const Lowpan::FragmentHeader &, bool)
 {
 }
 
-void MeshForwarder::LogFrame(const char *, const Mac::Frame &, Error)
-{
-}
-
-void MeshForwarder::LogFragmentFrameDrop(Error,
-                                         uint16_t,
-                                         const Mac::Address &,
-                                         const Mac::Address &,
-                                         const Lowpan::FragmentHeader &,
-                                         bool)
-{
-}
-
-void MeshForwarder::LogLowpanHcFrameDrop(Error, uint16_t, const Mac::Address &, const Mac::Address &, bool)
-{
-}
+void MeshForwarder::LogLowpanHcFrameDrop(Error, uint16_t, const Mac::Addresses &, bool) {}
 
 #endif // #if OT_SHOULD_LOG_AT( OT_LOG_LEVEL_NOTE)
 
diff --git a/src/core/thread/mesh_forwarder.hpp b/src/core/thread/mesh_forwarder.hpp
index e063c9d..9ee160b 100644
--- a/src/core/thread/mesh_forwarder.hpp
+++ b/src/core/thread/mesh_forwarder.hpp
@@ -334,6 +334,12 @@
     static constexpr uint8_t kMeshHeaderFrameMtu     = OT_RADIO_FRAME_MAX_SIZE; // Max MTU with a Mesh Header frame.
     static constexpr uint8_t kMeshHeaderFrameFcsSize = sizeof(uint16_t);        // Frame FCS size for Mesh Header frame.
 
+    // Hops left to use in lowpan mesh header: We use `kMaxRouteCost` as
+    // max hops between routers within Thread  mesh. We then add two
+    // for possibility of source or destination being a child
+    // (requiring one hop) and one as additional guard increment.
+    static constexpr uint8_t kMeshHeaderHopsLeft = Mle::kMaxRouteCost + 3;
+
     static constexpr uint32_t kTxDelayInterval = OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_INTERVAL; // In msec
 
 #if OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE
@@ -426,38 +432,35 @@
     };
 #endif // OPENTHREAD_FTD
 
-    void     SendIcmpErrorIfDstUnreach(const Message &     aMessage,
-                                       const Mac::Address &aMacSource,
-                                       const Mac::Address &aMacDest);
-    Error    CheckReachability(const FrameData &   aFrameData,
-                               const Mac::Address &aMeshSource,
-                               const Mac::Address &aMeshDest);
-    void     UpdateRoutes(const FrameData &aFrameData, const Mac::Address &aMeshSource, const Mac::Address &aMeshDest);
-    Error    FrameToMessage(const FrameData &   aFrameData,
-                            uint16_t            aDatagramSize,
-                            const Mac::Address &aMacSource,
-                            const Mac::Address &aMacDest,
-                            Message *&          aMessage);
+    void     SendIcmpErrorIfDstUnreach(const Message &aMessage, const Mac::Addresses &aMacAddrs);
+    Error    CheckReachability(const FrameData &aFrameData, const Mac::Addresses &aMeshAddrs);
+    void     UpdateRoutes(const FrameData &aFrameData, const Mac::Addresses &aMeshAddrs);
+    Error    FrameToMessage(const FrameData      &aFrameData,
+                            uint16_t              aDatagramSize,
+                            const Mac::Addresses &aMacAddrs,
+                            Message             *&aMessage);
     void     GetMacDestinationAddress(const Ip6::Address &aIp6Addr, Mac::Address &aMacAddr);
     void     GetMacSourceAddress(const Ip6::Address &aIp6Addr, Mac::Address &aMacAddr);
     Message *PrepareNextDirectTransmission(void);
     void     HandleMesh(FrameData &aFrameData, const Mac::Address &aMacSource, const ThreadLinkInfo &aLinkInfo);
-    void     HandleFragment(FrameData &           aFrameData,
-                            const Mac::Address &  aMacSource,
-                            const Mac::Address &  aMacDest,
-                            const ThreadLinkInfo &aLinkInfo);
-    void     HandleLowpanHC(const FrameData &     aFrameData,
-                            const Mac::Address &  aMacSource,
-                            const Mac::Address &  aMacDest,
-                            const ThreadLinkInfo &aLinkInfo);
-    uint16_t PrepareDataFrame(Mac::TxFrame &      aFrame,
-                              Message &           aMessage,
-                              const Mac::Address &aMacSource,
-                              const Mac::Address &aMacDest,
-                              bool                aAddMeshHeader = false,
-                              uint16_t            aMeshSource    = 0xffff,
-                              uint16_t            aMeshDest      = 0xffff,
-                              bool                aAddFragHeader = false);
+    void     HandleFragment(FrameData &aFrameData, const Mac::Addresses &aMacAddrs, const ThreadLinkInfo &aLinkInfo);
+    void HandleLowpanHC(const FrameData &aFrameData, const Mac::Addresses &aMacAddrs, const ThreadLinkInfo &aLinkInfo);
+
+    void PrepareMacHeaders(Mac::TxFrame             &aFrame,
+                           Mac::Frame::Type          aFrameType,
+                           const Mac::Addresses     &aMacAddr,
+                           const Mac::PanIds        &aPanIds,
+                           Mac::Frame::SecurityLevel aSecurityLevel,
+                           Mac::Frame::KeyIdMode     aKeyIdMode,
+                           const Message            *aMessage);
+
+    uint16_t PrepareDataFrame(Mac::TxFrame         &aFrame,
+                              Message              &aMessage,
+                              const Mac::Addresses &aMacAddrs,
+                              bool                  aAddMeshHeader = false,
+                              uint16_t              aMeshSource    = 0xffff,
+                              uint16_t              aMeshDest      = 0xffff,
+                              bool                  aAddFragHeader = false);
     void     PrepareEmptyFrame(Mac::TxFrame &aFrame, const Mac::Address &aMacDest, bool aAckRequest);
 
 #if OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE
@@ -487,7 +490,7 @@
 
     void          HandleReceivedFrame(Mac::RxFrame &aFrame);
     Mac::TxFrame *HandleFrameRequest(Mac::TxFrames &aTxFrames);
-    Neighbor *    UpdateNeighborOnSentFrame(Mac::TxFrame &      aFrame,
+    Neighbor     *UpdateNeighborOnSentFrame(Mac::TxFrame       &aFrame,
                                             Error               aError,
                                             const Mac::Address &aMacDest,
                                             bool                aIsDataPoll = false);
@@ -499,24 +502,19 @@
     void          UpdateSendMessage(Error aFrameTxError, Mac::Address &aMacDest, Neighbor *aNeighbor);
     void          RemoveMessageIfNoPendingTx(Message &aMessage);
 
-    void        HandleTimeTick(void);
-    static void ScheduleTransmissionTask(Tasklet &aTasklet);
-    void        ScheduleTransmissionTask(void);
+    void HandleTimeTick(void);
+    void ScheduleTransmissionTask(void);
 
-    Error GetFramePriority(const FrameData &   aFrameData,
-                           const Mac::Address &aMacSource,
-                           const Mac::Address &aMacDest,
-                           Message::Priority & aPriority);
+    Error GetFramePriority(const FrameData &aFrameData, const Mac::Addresses &aMacAddrs, Message::Priority &aPriority);
     Error GetFragmentPriority(Lowpan::FragmentHeader &aFragmentHeader,
                               uint16_t                aSrcRloc16,
-                              Message::Priority &     aPriority);
-    void  GetForwardFramePriority(const FrameData &   aFrameData,
-                                  const Mac::Address &aMeshSource,
-                                  const Mac::Address &aMeshDest,
-                                  Message::Priority & aPriority);
+                              Message::Priority      &aPriority);
+    void  GetForwardFramePriority(const FrameData      &aFrameData,
+                                  const Mac::Addresses &aMeshAddrs,
+                                  Message::Priority    &aPriority);
 
-    bool     CalcIePresent(const Message *aMessage);
-    uint16_t CalcFrameVersion(const Neighbor *aNeighbor, bool aIePresent);
+    bool                CalcIePresent(const Message *aMessage);
+    Mac::Frame::Version CalcFrameVersion(const Neighbor *aNeighbor, bool aIePresent) const;
 #if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
     void AppendHeaderIe(const Message *aMessage, Mac::TxFrame &aFrame);
 #endif
@@ -525,26 +523,20 @@
     void ResumeMessageTransmissions(void);
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
-    static void HandleTxDelayTimer(Timer &aTimer);
-    void        HandleTxDelayTimer(void);
+    void HandleTxDelayTimer(void);
 #endif
 
     void LogMessage(MessageAction       aAction,
-                    const Message &     aMessage,
+                    const Message      &aMessage,
                     Error               aError   = kErrorNone,
                     const Mac::Address *aAddress = nullptr);
     void LogFrame(const char *aActionText, const Mac::Frame &aFrame, Error aError);
     void LogFragmentFrameDrop(Error                         aError,
                               uint16_t                      aFrameLength,
-                              const Mac::Address &          aMacSource,
-                              const Mac::Address &          aMacDest,
+                              const Mac::Addresses         &aMacAddrs,
                               const Lowpan::FragmentHeader &aFragmentHeader,
                               bool                          aIsSecure);
-    void LogLowpanHcFrameDrop(Error               aError,
-                              uint16_t            aFrameLength,
-                              const Mac::Address &aMacSource,
-                              const Mac::Address &aMacDest,
-                              bool                aIsSecure);
+    void LogLowpanHcFrameDrop(Error aError, uint16_t aFrameLength, const Mac::Addresses &aMacAddrs, bool aIsSecure);
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_NOTE)
     const char *MessageActionToString(MessageAction aAction, Error aError);
@@ -552,32 +544,36 @@
 
 #if OPENTHREAD_FTD
     Error LogMeshFragmentHeader(MessageAction       aAction,
-                                const Message &     aMessage,
+                                const Message      &aMessage,
                                 const Mac::Address *aMacAddress,
                                 Error               aError,
-                                uint16_t &          aOffset,
-                                Mac::Address &      aMeshSource,
-                                Mac::Address &      aMeshDest,
+                                uint16_t           &aOffset,
+                                Mac::Addresses     &aMeshAddrs,
                                 LogLevel            aLogLevel);
-    void  LogMeshIpHeader(const Message &     aMessage,
-                          uint16_t            aOffset,
-                          const Mac::Address &aMeshSource,
-                          const Mac::Address &aMeshDest,
-                          LogLevel            aLogLevel);
+    void  LogMeshIpHeader(const Message        &aMessage,
+                          uint16_t              aOffset,
+                          const Mac::Addresses &aMeshAddrs,
+                          LogLevel              aLogLevel);
     void  LogMeshMessage(MessageAction       aAction,
-                         const Message &     aMessage,
+                         const Message      &aMessage,
                          const Mac::Address *aAddress,
                          Error               aError,
                          LogLevel            aLogLevel);
 #endif
     void LogIp6SourceDestAddresses(const Ip6::Headers &aHeaders, LogLevel aLogLevel);
     void LogIp6Message(MessageAction       aAction,
-                       const Message &     aMessage,
+                       const Message      &aMessage,
                        const Mac::Address *aAddress,
                        Error               aError,
                        LogLevel            aLogLevel);
 #endif // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_NOTE)
 
+    using TxTask = TaskletIn<MeshForwarder, &MeshForwarder::ScheduleTransmissionTask>;
+
+#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
+    using TxDelayTimer = TimerMilliIn<MeshForwarder, &MeshForwarder::HandleTxDelayTimer>;
+#endif
+
     PriorityQueue mSendQueue;
     MessageQueue  mReassemblyList;
     uint16_t      mFragTag;
@@ -585,20 +581,19 @@
 
     Message *mSendMessage;
 
-    Mac::Address mMacSource;
-    Mac::Address mMacDest;
-    uint16_t     mMeshSource;
-    uint16_t     mMeshDest;
-    bool         mAddMeshHeader : 1;
-    bool         mEnabled : 1;
-    bool         mTxPaused : 1;
-    bool         mSendBusy : 1;
+    Mac::Addresses mMacAddrs;
+    uint16_t       mMeshSource;
+    uint16_t       mMeshDest;
+    bool           mAddMeshHeader : 1;
+    bool           mEnabled : 1;
+    bool           mTxPaused : 1;
+    bool           mSendBusy : 1;
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
-    bool       mDelayNextTx : 1;
-    TimerMilli mTxDelayTimer;
+    bool         mDelayNextTx : 1;
+    TxDelayTimer mTxDelayTimer;
 #endif
 
-    Tasklet mScheduleTransmissionTask;
+    TxTask mScheduleTransmissionTask;
 
     otIpCounters mIpCounters;
 
diff --git a/src/core/thread/mesh_forwarder_ftd.cpp b/src/core/thread/mesh_forwarder_ftd.cpp
index 9b5cdc3..ffec4c7 100644
--- a/src/core/thread/mesh_forwarder_ftd.cpp
+++ b/src/core/thread/mesh_forwarder_ftd.cpp
@@ -36,6 +36,7 @@
 #if OPENTHREAD_FTD
 
 #include "common/locator_getters.hpp"
+#include "common/num_utils.hpp"
 #include "meshcop/meshcop.hpp"
 #include "net/ip6.hpp"
 #include "net/tcp6.hpp"
@@ -49,7 +50,6 @@
 {
     Mle::MleRouter &mle   = Get<Mle::MleRouter>();
     Error           error = kErrorNone;
-    Neighbor *      neighbor;
 
     aMessage.SetOffset(0);
     aMessage.SetDatagramTag(0);
@@ -103,31 +103,32 @@
                 }
             }
         }
-        else if ((neighbor = Get<NeighborTable>().FindNeighbor(ip6Header.GetDestination())) != nullptr &&
-                 !neighbor->IsRxOnWhenIdle() && !aMessage.IsDirectTransmission())
+        else // Destination is unicast
         {
-            // destined for a sleepy child
-            Child &child = *static_cast<Child *>(neighbor);
-            mIndirectSender.AddMessageForSleepyChild(aMessage, child);
-        }
-        else
-        {
-            // schedule direct transmission
-            aMessage.SetDirectTransmission();
+            Neighbor *neighbor = Get<NeighborTable>().FindNeighbor(ip6Header.GetDestination());
+
+            if ((neighbor != nullptr) && !neighbor->IsRxOnWhenIdle() && !aMessage.IsDirectTransmission() &&
+                Get<ChildTable>().Contains(*neighbor))
+            {
+                // Destined for a sleepy child
+                mIndirectSender.AddMessageForSleepyChild(aMessage, *static_cast<Child *>(neighbor));
+            }
+            else
+            {
+                aMessage.SetDirectTransmission();
+            }
         }
 
         break;
     }
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
     case Message::kTypeSupervision:
     {
-        Child *child = Get<Utils::ChildSupervisor>().GetDestination(aMessage);
+        Child *child = Get<ChildSupervisor>().GetDestination(aMessage);
         OT_ASSERT((child != nullptr) && !child->IsRxOnWhenIdle());
         mIndirectSender.AddMessageForSleepyChild(aMessage, *child);
         break;
     }
-#endif
 
     default:
         aMessage.SetDirectTransmission();
@@ -184,7 +185,7 @@
             hopLimit++;
             message.Write(Ip6::Header::kHopLimitFieldOffset, hopLimit);
 
-            IgnoreError(Get<Ip6::Ip6>().HandleDatagram(message, nullptr, nullptr, /* aFromHost */ false));
+            IgnoreError(Get<Ip6::Ip6>().HandleDatagram(message, Ip6::Ip6::kFromHostAllowLoopBack));
             continue;
         }
 #endif
@@ -282,7 +283,7 @@
 
                 IgnoreError(message.Read(0, ip6header));
 
-                if (&aChild == static_cast<Child *>(Get<NeighborTable>().FindNeighbor(ip6header.GetDestination())))
+                if (&aChild == Get<NeighborTable>().FindNeighbor(ip6header.GetDestination()))
                 {
                     message.ClearDirectTransmission();
                 }
@@ -296,7 +297,7 @@
 
                 IgnoreError(meshHeader.ParseFrom(message));
 
-                if (&aChild == static_cast<Child *>(Get<NeighborTable>().FindNeighbor(meshHeader.GetDestination())))
+                if (&aChild == Get<NeighborTable>().FindNeighbor(meshHeader.GetDestination()))
                 {
                     message.ClearDirectTransmission();
                 }
@@ -346,31 +347,13 @@
 
 void MeshForwarder::SendMesh(Message &aMessage, Mac::TxFrame &aFrame)
 {
-    uint16_t fcf;
-    bool     iePresent = CalcIePresent(&aMessage);
+    Mac::PanIds panIds;
 
-    // initialize MAC header
-    fcf = Mac::Frame::kFcfFrameData | Mac::Frame::kFcfPanidCompression | Mac::Frame::kFcfDstAddrShort |
-          Mac::Frame::kFcfSrcAddrShort | Mac::Frame::kFcfAckRequest | Mac::Frame::kFcfSecurityEnabled;
+    panIds.mSource      = Get<Mac::Mac>().GetPanId();
+    panIds.mDestination = Get<Mac::Mac>().GetPanId();
 
-    if (iePresent)
-    {
-        fcf |= Mac::Frame::kFcfIePresent;
-    }
-
-    fcf |= CalcFrameVersion(Get<NeighborTable>().FindNeighbor(mMacDest), iePresent);
-
-    aFrame.InitMacHeader(fcf, Mac::Frame::kKeyIdMode1 | Mac::Frame::kSecEncMic32);
-    aFrame.SetDstPanId(Get<Mac::Mac>().GetPanId());
-    aFrame.SetDstAddr(mMacDest.GetShort());
-    aFrame.SetSrcAddr(mMacSource.GetShort());
-
-#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
-    if (iePresent)
-    {
-        AppendHeaderIe(&aMessage, aFrame);
-    }
-#endif
+    PrepareMacHeaders(aFrame, Mac::Frame::kTypeData, mMacAddrs, panIds, Mac::Frame::kSecurityEncMic32,
+                      Mac::Frame::kKeyIdMode1, &aMessage);
 
     // write payload
     OT_ASSERT(aMessage.GetLength() <= aFrame.GetMaxPayloadLength());
@@ -384,7 +367,7 @@
 {
     Error              error = kErrorNone;
     Lowpan::MeshHeader meshHeader;
-    Neighbor *         neighbor;
+    Neighbor          *neighbor;
     uint16_t           nextHop;
 
     IgnoreError(meshHeader.ParseFrom(aMessage));
@@ -405,15 +388,15 @@
         ExitNow(error = kErrorDrop);
     }
 
-    mMacDest.SetShort(neighbor->GetRloc16());
-    mMacSource.SetShort(Get<Mac::Mac>().GetShortAddress());
+    mMacAddrs.mDestination.SetShort(neighbor->GetRloc16());
+    mMacAddrs.mSource.SetShort(Get<Mac::Mac>().GetShortAddress());
 
     mAddMeshHeader = true;
     mMeshDest      = meshHeader.GetDestination();
     mMeshSource    = meshHeader.GetSource();
 
 #if OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
-    if (mMacDest.GetShort() != mMeshDest)
+    if (mMacAddrs.mDestination.GetShort() != mMeshDest)
     {
         mDelayNextTx = true;
     }
@@ -425,44 +408,12 @@
 
 void MeshForwarder::EvaluateRoutingCost(uint16_t aDest, uint8_t &aBestCost, uint16_t &aBestDest) const
 {
-    const Neighbor *neighbor;
-    uint8_t         curCost = 0x00;
+    uint8_t cost = Get<RouterTable>().GetPathCost(aDest);
 
-    // Path cost
-    curCost = Get<Mle::MleRouter>().GetCost(aDest);
-
-    if (!Mle::MleRouter::IsActiveRouter(aDest))
-    {
-        // Assume best link between remote child server and its parent.
-        curCost += 1;
-    }
-
-    // Cost if the server is direct neighbor.
-    neighbor = Get<NeighborTable>().FindNeighbor(aDest);
-
-    if (neighbor != nullptr && neighbor->IsStateValid())
-    {
-        uint8_t cost;
-
-        if (!Mle::MleRouter::IsActiveRouter(aDest))
-        {
-            // Cost calculated only from Link Quality In as the parent only maintains
-            // one-direction link info.
-            cost = Mle::MleRouter::LinkQualityToCost(neighbor->GetLinkInfo().GetLinkQuality());
-        }
-        else
-        {
-            cost = Get<Mle::MleRouter>().GetLinkCost(Mle::Mle::RouterIdFromRloc16(aDest));
-        }
-
-        // Choose the minimum cost
-        curCost = OT_MIN(curCost, cost);
-    }
-
-    if ((aBestDest == Mac::kShortAddrInvalid) || (curCost < aBestCost))
+    if ((aBestDest == Mac::kShortAddrInvalid) || (cost < aBestCost))
     {
         aBestDest = aDest;
-        aBestCost = curCost;
+        aBestCost = cost;
     }
 }
 
@@ -532,14 +483,13 @@
     }
     }
 
-    routerId = Mle::Mle::RouterIdFromRloc16(bestDest);
+    routerId = Mle::RouterIdFromRloc16(bestDest);
 
-    if (!(Mle::Mle::IsActiveRouter(bestDest) ||
-          Mle::Mle::Rloc16FromRouterId(routerId) == Get<Mle::MleRouter>().GetRloc16()))
+    if (!(Mle::IsActiveRouter(bestDest) || Mle::Rloc16FromRouterId(routerId) == Get<Mle::MleRouter>().GetRloc16()))
     {
         // if agent is neither active router nor child of this device
         // use the parent of the ED Agent as Dest
-        bestDest = Mle::Mle::Rloc16FromRouterId(routerId);
+        bestDest = Mle::Rloc16FromRouterId(routerId);
     }
 
     aMeshDest = bestDest;
@@ -552,7 +502,7 @@
 {
     Mle::MleRouter &mle   = Get<Mle::MleRouter>();
     Error           error = kErrorNone;
-    Neighbor *      neighbor;
+    Neighbor       *neighbor;
 
     if (aMessage.GetOffset() > 0)
     {
@@ -561,7 +511,7 @@
     else if (mle.IsRoutingLocator(ip6Header.GetDestination()))
     {
         uint16_t rloc16 = ip6Header.GetDestination().GetIid().GetLocator();
-        VerifyOrExit(mle.IsRouterIdValid(Mle::Mle::RouterIdFromRloc16(rloc16)), error = kErrorDrop);
+        VerifyOrExit(mle.IsRouterIdValid(Mle::RouterIdFromRloc16(rloc16)), error = kErrorDrop);
         mMeshDest = rloc16;
     }
     else if (mle.IsAnycastLocator(ip6Header.GetDestination()))
@@ -570,7 +520,7 @@
 
         if (aloc16 == Mle::kAloc16Leader)
         {
-            mMeshDest = Mle::Mle::Rloc16FromRouterId(mle.GetLeaderId());
+            mMeshDest = Mle::Rloc16FromRouterId(mle.GetLeaderId());
         }
         else if (aloc16 <= Mle::kAloc16DhcpAgentEnd)
         {
@@ -615,8 +565,8 @@
     }
     else
     {
-        IgnoreError(Get<NetworkData::Leader>().RouteLookup(ip6Header.GetSource(), ip6Header.GetDestination(), nullptr,
-                                                           &mMeshDest));
+        IgnoreError(
+            Get<NetworkData::Leader>().RouteLookup(ip6Header.GetSource(), ip6Header.GetDestination(), mMeshDest));
     }
 
     VerifyOrExit(mMeshDest != Mac::kShortAddrInvalid, error = kErrorDrop);
@@ -625,12 +575,12 @@
 
     SuccessOrExit(error = mle.CheckReachability(mMeshDest, ip6Header));
     aMessage.SetMeshDest(mMeshDest);
-    mMacDest.SetShort(mle.GetNextHop(mMeshDest));
+    mMacAddrs.mDestination.SetShort(mle.GetNextHop(mMeshDest));
 
-    if (mMacDest.GetShort() != mMeshDest)
+    if (mMacAddrs.mDestination.GetShort() != mMeshDest)
     {
         // destination is not neighbor
-        mMacSource.SetShort(mMeshSource);
+        mMacAddrs.mSource.SetShort(mMeshSource);
         mAddMeshHeader = true;
 #if OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
         mDelayNextTx = true;
@@ -641,17 +591,15 @@
     return error;
 }
 
-void MeshForwarder::SendIcmpErrorIfDstUnreach(const Message &     aMessage,
-                                              const Mac::Address &aMacSource,
-                                              const Mac::Address &aMacDest)
+void MeshForwarder::SendIcmpErrorIfDstUnreach(const Message &aMessage, const Mac::Addresses &aMacAddrs)
 {
     Error        error;
     Ip6::Headers ip6Headers;
-    Child *      child;
+    Child       *child;
 
-    VerifyOrExit(aMacSource.IsShort() && aMacDest.IsShort());
+    VerifyOrExit(aMacAddrs.mSource.IsShort() && aMacAddrs.mDestination.IsShort());
 
-    child = Get<ChildTable>().FindChild(aMacSource.GetShort(), Child::kInStateAnyExceptInvalid);
+    child = Get<ChildTable>().FindChild(aMacAddrs.mSource.GetShort(), Child::kInStateAnyExceptInvalid);
     VerifyOrExit((child == nullptr) || child->IsFullThreadDevice());
 
     SuccessOrExit(ip6Headers.ParseFrom(aMessage));
@@ -659,25 +607,23 @@
     VerifyOrExit(!ip6Headers.GetDestinationAddress().IsMulticast() &&
                  Get<NetworkData::Leader>().IsOnMesh(ip6Headers.GetDestinationAddress()));
 
-    error = Get<Mle::MleRouter>().CheckReachability(aMacDest.GetShort(), ip6Headers.GetIp6Header());
+    error = Get<Mle::MleRouter>().CheckReachability(aMacAddrs.mDestination.GetShort(), ip6Headers.GetIp6Header());
 
     if (error == kErrorNoRoute)
     {
-        SendDestinationUnreachable(aMacSource.GetShort(), ip6Headers);
+        SendDestinationUnreachable(aMacAddrs.mSource.GetShort(), ip6Headers);
     }
 
 exit:
     return;
 }
 
-Error MeshForwarder::CheckReachability(const FrameData &   aFrameData,
-                                       const Mac::Address &aMeshSource,
-                                       const Mac::Address &aMeshDest)
+Error MeshForwarder::CheckReachability(const FrameData &aFrameData, const Mac::Addresses &aMeshAddrs)
 {
     Error        error;
     Ip6::Headers ip6Headers;
 
-    error = ip6Headers.DecompressFrom(aFrameData, aMeshSource, aMeshDest, GetInstance());
+    error = ip6Headers.DecompressFrom(aFrameData, aMeshAddrs, GetInstance());
 
     if (error == kErrorNotFound)
     {
@@ -685,11 +631,11 @@
         ExitNow(error = kErrorNone);
     }
 
-    error = Get<Mle::MleRouter>().CheckReachability(aMeshDest.GetShort(), ip6Headers.GetIp6Header());
+    error = Get<Mle::MleRouter>().CheckReachability(aMeshAddrs.mDestination.GetShort(), ip6Headers.GetIp6Header());
 
     if (error == kErrorNoRoute)
     {
-        SendDestinationUnreachable(aMeshSource.GetShort(), ip6Headers);
+        SendDestinationUnreachable(aMeshAddrs.mSource.GetShort(), ip6Headers);
     }
 
 exit:
@@ -710,9 +656,8 @@
 void MeshForwarder::HandleMesh(FrameData &aFrameData, const Mac::Address &aMacSource, const ThreadLinkInfo &aLinkInfo)
 {
     Error              error   = kErrorNone;
-    Message *          message = nullptr;
-    Mac::Address       meshDest;
-    Mac::Address       meshSource;
+    Message           *message = nullptr;
+    Mac::Addresses     meshAddrs;
     Lowpan::MeshHeader meshHeader;
 
     // Security Check: only process Mesh Header frames that had security enabled.
@@ -720,21 +665,21 @@
 
     SuccessOrExit(error = meshHeader.ParseFrom(aFrameData));
 
-    meshSource.SetShort(meshHeader.GetSource());
-    meshDest.SetShort(meshHeader.GetDestination());
+    meshAddrs.mSource.SetShort(meshHeader.GetSource());
+    meshAddrs.mDestination.SetShort(meshHeader.GetDestination());
 
-    UpdateRoutes(aFrameData, meshSource, meshDest);
+    UpdateRoutes(aFrameData, meshAddrs);
 
-    if (meshDest.GetShort() == Get<Mac::Mac>().GetShortAddress() ||
-        Get<Mle::MleRouter>().IsMinimalChild(meshDest.GetShort()))
+    if (meshAddrs.mDestination.GetShort() == Get<Mac::Mac>().GetShortAddress() ||
+        Get<Mle::MleRouter>().IsMinimalChild(meshAddrs.mDestination.GetShort()))
     {
         if (Lowpan::FragmentHeader::IsFragmentHeader(aFrameData))
         {
-            HandleFragment(aFrameData, meshSource, meshDest, aLinkInfo);
+            HandleFragment(aFrameData, meshAddrs, aLinkInfo);
         }
         else if (Lowpan::Lowpan::IsLowpanHc(aFrameData))
         {
-            HandleLowpanHC(aFrameData, meshSource, meshDest, aLinkInfo);
+            HandleLowpanHC(aFrameData, meshAddrs, aLinkInfo);
         }
         else
         {
@@ -745,13 +690,13 @@
     {
         Message::Priority priority = Message::kPriorityNormal;
 
-        Get<Mle::MleRouter>().ResolveRoutingLoops(aMacSource.GetShort(), meshDest.GetShort());
+        Get<Mle::MleRouter>().ResolveRoutingLoops(aMacSource.GetShort(), meshAddrs.mDestination.GetShort());
 
-        SuccessOrExit(error = CheckReachability(aFrameData, meshSource, meshDest));
+        SuccessOrExit(error = CheckReachability(aFrameData, meshAddrs));
 
         meshHeader.DecrementHopsLeft();
 
-        GetForwardFramePriority(aFrameData, meshSource, meshDest, priority);
+        GetForwardFramePriority(aFrameData, meshAddrs, priority);
         message =
             Get<MessagePool>().Allocate(Message::kType6lowpan, /* aReserveHeader */ 0, Message::Settings(priority));
         VerifyOrExit(message != nullptr, error = kErrorNoBufs);
@@ -790,16 +735,14 @@
     }
 }
 
-void MeshForwarder::UpdateRoutes(const FrameData &   aFrameData,
-                                 const Mac::Address &aMeshSource,
-                                 const Mac::Address &aMeshDest)
+void MeshForwarder::UpdateRoutes(const FrameData &aFrameData, const Mac::Addresses &aMeshAddrs)
 {
     Ip6::Headers ip6Headers;
-    Neighbor *   neighbor;
+    Neighbor    *neighbor;
 
-    VerifyOrExit(!aMeshDest.IsBroadcast() && aMeshSource.IsShort());
+    VerifyOrExit(!aMeshAddrs.mDestination.IsBroadcast() && aMeshAddrs.mSource.IsShort());
 
-    SuccessOrExit(ip6Headers.DecompressFrom(aFrameData, aMeshSource, aMeshDest, GetInstance()));
+    SuccessOrExit(ip6Headers.DecompressFrom(aFrameData, aMeshAddrs, GetInstance()));
 
     if (!ip6Headers.GetSourceAddress().GetIid().IsLocator() &&
         Get<NetworkData::Leader>().IsOnMesh(ip6Headers.GetSourceAddress()))
@@ -808,14 +751,14 @@
         // inspecting packets being received only for on mesh
         // addresses.
 
-        Get<AddressResolver>().UpdateSnoopedCacheEntry(ip6Headers.GetSourceAddress(), aMeshSource.GetShort(),
-                                                       aMeshDest.GetShort());
+        Get<AddressResolver>().UpdateSnoopedCacheEntry(ip6Headers.GetSourceAddress(), aMeshAddrs.mSource.GetShort(),
+                                                       aMeshAddrs.mDestination.GetShort());
     }
 
     neighbor = Get<NeighborTable>().FindNeighbor(ip6Headers.GetSourceAddress());
     VerifyOrExit(neighbor != nullptr && !neighbor->IsFullThreadDevice());
 
-    if (!Mle::Mle::RouterIdMatch(aMeshSource.GetShort(), Get<Mac::Mac>().GetShortAddress()))
+    if (!Mle::RouterIdMatch(aMeshAddrs.mSource.GetShort(), Get<Mac::Mac>().GetShortAddress()))
     {
         Get<Mle::MleRouter>().RemoveNeighbor(*neighbor);
     }
@@ -826,7 +769,7 @@
 
 bool MeshForwarder::FragmentPriorityList::UpdateOnTimeTick(void)
 {
-    bool contineRxingTicks = false;
+    bool continueRxingTicks = false;
 
     for (Entry &entry : mEntries)
     {
@@ -836,12 +779,12 @@
 
             if (!entry.IsExpired())
             {
-                contineRxingTicks = true;
+                continueRxingTicks = true;
             }
         }
     }
 
-    return contineRxingTicks;
+    return continueRxingTicks;
 }
 
 void MeshForwarder::UpdateFragmentPriority(Lowpan::FragmentHeader &aFragmentHeader,
@@ -925,7 +868,7 @@
 
 Error MeshForwarder::GetFragmentPriority(Lowpan::FragmentHeader &aFragmentHeader,
                                          uint16_t                aSrcRloc16,
-                                         Message::Priority &     aPriority)
+                                         Message::Priority      &aPriority)
 {
     Error                        error = kErrorNone;
     FragmentPriorityList::Entry *entry;
@@ -938,10 +881,9 @@
     return error;
 }
 
-void MeshForwarder::GetForwardFramePriority(const FrameData &   aFrameData,
-                                            const Mac::Address &aMeshSource,
-                                            const Mac::Address &aMeshDest,
-                                            Message::Priority & aPriority)
+void MeshForwarder::GetForwardFramePriority(const FrameData      &aFrameData,
+                                            const Mac::Addresses &aMeshAddrs,
+                                            Message::Priority    &aPriority)
 {
     Error                  error      = kErrorNone;
     FrameData              frameData  = aFrameData;
@@ -955,22 +897,23 @@
         if (fragmentHeader.GetDatagramOffset() > 0)
         {
             // Get priority from the pre-buffered info
-            ExitNow(error = GetFragmentPriority(fragmentHeader, aMeshSource.GetShort(), aPriority));
+            ExitNow(error = GetFragmentPriority(fragmentHeader, aMeshAddrs.mSource.GetShort(), aPriority));
         }
     }
 
     // Get priority from IPv6 header or UDP destination port directly
-    error = GetFramePriority(frameData, aMeshSource, aMeshDest, aPriority);
+    error = GetFramePriority(frameData, aMeshAddrs, aPriority);
 
 exit:
     if (error != kErrorNone)
     {
-        LogNote("Failed to get forwarded frame priority, error:%s, len:%d, src:%d, dst:%s", ErrorToString(error),
-                frameData.GetLength(), aMeshSource.ToString().AsCString(), aMeshDest.ToString().AsCString());
+        LogNote("Failed to get forwarded frame priority, error:%s, len:%d, src:%s, dst:%s", ErrorToString(error),
+                frameData.GetLength(), aMeshAddrs.mSource.ToString().AsCString(),
+                aMeshAddrs.mDestination.ToString().AsCString());
     }
     else if (isFragment)
     {
-        UpdateFragmentPriority(fragmentHeader, frameData.GetLength(), aMeshSource.GetShort(), aPriority);
+        UpdateFragmentPriority(fragmentHeader, frameData.GetLength(), aMeshAddrs.mSource.GetShort(), aPriority);
     }
 }
 
@@ -979,12 +922,11 @@
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_NOTE)
 
 Error MeshForwarder::LogMeshFragmentHeader(MessageAction       aAction,
-                                           const Message &     aMessage,
+                                           const Message      &aMessage,
                                            const Mac::Address *aMacAddress,
                                            Error               aError,
-                                           uint16_t &          aOffset,
-                                           Mac::Address &      aMeshSource,
-                                           Mac::Address &      aMeshDest,
+                                           uint16_t           &aOffset,
+                                           Mac::Addresses     &aMeshAddrs,
                                            LogLevel            aLogLevel)
 {
     Error                  error             = kErrorFailed;
@@ -994,12 +936,12 @@
     Lowpan::FragmentHeader fragmentHeader;
     uint16_t               headerLength;
     bool                   shouldLogRadio = false;
-    const char *           radioString    = "";
+    const char            *radioString    = "";
 
     SuccessOrExit(meshHeader.ParseFrom(aMessage, headerLength));
 
-    aMeshSource.SetShort(meshHeader.GetSource());
-    aMeshDest.SetShort(meshHeader.GetDestination());
+    aMeshAddrs.mSource.SetShort(meshHeader.GetSource());
+    aMeshAddrs.mDestination.SetShort(meshHeader.GetDestination());
 
     aOffset = headerLength;
 
@@ -1019,9 +961,10 @@
     LogAt(aLogLevel, "%s mesh frame, len:%d%s%s, msrc:%s, mdst:%s, hops:%d, frag:%s, sec:%s%s%s%s%s%s%s",
           MessageActionToString(aAction, aError), aMessage.GetLength(),
           (aMacAddress == nullptr) ? "" : ((aAction == kMessageReceive) ? ", from:" : ", to:"),
-          (aMacAddress == nullptr) ? "" : aMacAddress->ToString().AsCString(), aMeshSource.ToString().AsCString(),
-          aMeshDest.ToString().AsCString(), meshHeader.GetHopsLeft() + ((aAction == kMessageReceive) ? 1 : 0),
-          ToYesNo(hasFragmentHeader), ToYesNo(aMessage.IsLinkSecurityEnabled()),
+          (aMacAddress == nullptr) ? "" : aMacAddress->ToString().AsCString(),
+          aMeshAddrs.mSource.ToString().AsCString(), aMeshAddrs.mDestination.ToString().AsCString(),
+          meshHeader.GetHopsLeft() + ((aAction == kMessageReceive) ? 1 : 0), ToYesNo(hasFragmentHeader),
+          ToYesNo(aMessage.IsLinkSecurityEnabled()),
           (aError == kErrorNone) ? "" : ", error:", (aError == kErrorNone) ? "" : ErrorToString(aError),
           shouldLogRss ? ", rss:" : "", shouldLogRss ? aMessage.GetRssAverager().ToString().AsCString() : "",
           shouldLogRadio ? ", radio:" : "", radioString);
@@ -1040,15 +983,14 @@
     return error;
 }
 
-void MeshForwarder::LogMeshIpHeader(const Message &     aMessage,
-                                    uint16_t            aOffset,
-                                    const Mac::Address &aMeshSource,
-                                    const Mac::Address &aMeshDest,
-                                    LogLevel            aLogLevel)
+void MeshForwarder::LogMeshIpHeader(const Message        &aMessage,
+                                    uint16_t              aOffset,
+                                    const Mac::Addresses &aMeshAddrs,
+                                    LogLevel              aLogLevel)
 {
     Ip6::Headers headers;
 
-    SuccessOrExit(headers.DecompressFrom(aMessage, aOffset, aMeshSource, aMeshDest));
+    SuccessOrExit(headers.DecompressFrom(aMessage, aOffset, aMeshAddrs));
 
     LogAt(aLogLevel, "    IPv6 %s msg, chksum:%04x, ecn:%s, prio:%s", Ip6::Ip6::IpProtoToString(headers.GetIpProto()),
           headers.GetChecksum(), Ip6::Ip6::EcnToString(headers.GetEcn()), MessagePriorityToString(aMessage));
@@ -1060,17 +1002,15 @@
 }
 
 void MeshForwarder::LogMeshMessage(MessageAction       aAction,
-                                   const Message &     aMessage,
+                                   const Message      &aMessage,
                                    const Mac::Address *aMacAddress,
                                    Error               aError,
                                    LogLevel            aLogLevel)
 {
-    uint16_t     offset;
-    Mac::Address meshSource;
-    Mac::Address meshDest;
+    uint16_t       offset;
+    Mac::Addresses meshAddrs;
 
-    SuccessOrExit(
-        LogMeshFragmentHeader(aAction, aMessage, aMacAddress, aError, offset, meshSource, meshDest, aLogLevel));
+    SuccessOrExit(LogMeshFragmentHeader(aAction, aMessage, aMacAddress, aError, offset, meshAddrs, aLogLevel));
 
     // When log action is `kMessageTransmit` we do not include
     // the IPv6 header info in the logs, as the same info is
@@ -1079,7 +1019,7 @@
 
     VerifyOrExit(aAction != kMessageTransmit);
 
-    LogMeshIpHeader(aMessage, offset, meshSource, meshDest, aLogLevel);
+    LogMeshIpHeader(aMessage, offset, meshAddrs, aLogLevel);
 
 exit:
     return;
diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp
index 451aa92..bd65101 100644
--- a/src/core/thread/mle.cpp
+++ b/src/core/thread/mle.cpp
@@ -43,6 +43,7 @@
 #include "common/encoding.hpp"
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 #include "common/serial_number.hpp"
 #include "common/settings.hpp"
@@ -56,6 +57,7 @@
 #include "thread/mle_router.hpp"
 #include "thread/thread_netif.hpp"
 #include "thread/time_sync_service.hpp"
+#include "thread/version.hpp"
 
 using ot::Encoding::BigEndian::HostSwap16;
 
@@ -71,6 +73,7 @@
 Mle::Mle(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mRetrieveNewNetworkData(false)
+    , mRequestRouteTlv(false)
     , mRole(kRoleDisabled)
     , mNeighborTable(aInstance)
     , mDeviceMode(DeviceMode::kModeRxOnWhenIdle)
@@ -78,54 +81,41 @@
     , mReattachState(kReattachStop)
     , mAttachCounter(0)
     , mAnnounceDelay(kAnnounceTimeout)
-    , mAttachTimer(aInstance, Mle::HandleAttachTimer)
-    , mDelayedResponseTimer(aInstance, Mle::HandleDelayedResponseTimer)
-    , mMessageTransmissionTimer(aInstance, Mle::HandleMessageTransmissionTimer)
-    , mDetachGracefullyTimer(aInstance, Mle::HandleDetachGracefullyTimer)
-    , mParentLeaderCost(0)
-    , mDetachGracefullyCallback(nullptr)
-    , mDetachGracefullyContext(nullptr)
+    , mAttachTimer(aInstance)
+    , mDelayedResponseTimer(aInstance)
+    , mMessageTransmissionTimer(aInstance)
+#if OPENTHREAD_FTD
+    , mWasLeader(false)
+#endif
     , mAttachMode(kAnyPartition)
-    , mParentPriority(0)
-    , mParentLinkQuality3(0)
-    , mParentLinkQuality2(0)
-    , mParentLinkQuality1(0)
-    , mParentSedBufferSize(0)
-    , mParentSedDatagramCount(0)
     , mChildUpdateAttempts(0)
     , mChildUpdateRequestState(kChildUpdateRequestNone)
     , mDataRequestAttempts(0)
     , mDataRequestState(kDataRequestNone)
     , mAddressRegistrationMode(kAppendAllAddresses)
     , mHasRestored(false)
-    , mParentLinkMargin(0)
-    , mParentIsSingleton(false)
     , mReceivedResponseFromParent(false)
+    , mInitiallyAttachedAsSleepy(false)
     , mSocket(aInstance)
     , mTimeout(kMleEndDeviceTimeout)
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    , mCslTimeout(OPENTHREAD_CONFIG_CSL_TIMEOUT)
+    , mCslTimeout(kDefaultCslTimeout)
 #endif
+    , mRloc16(Mac::kShortAddrInvalid)
     , mPreviousParentRloc(Mac::kShortAddrInvalid)
 #if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
-    , mParentSearchIsInBackoff(false)
-    , mParentSearchBackoffWasCanceled(false)
-    , mParentSearchRecentlyDetached(false)
-    , mParentSearchBackoffCancelTime(0)
-    , mParentSearchTimer(aInstance, Mle::HandleParentSearchTimer)
+    , mParentSearch(aInstance)
 #endif
     , mAnnounceChannel(0)
     , mAlternateChannel(0)
     , mAlternatePanId(Mac::kPanIdBroadcast)
     , mAlternateTimestamp(0)
-    , mParentResponseCb(nullptr)
-    , mParentResponseCbContext(nullptr)
+    , mDetachGracefullyTimer(aInstance)
 {
     mParent.Init(aInstance);
     mParentCandidate.Init(aInstance);
 
     mLeaderData.Clear();
-    mParentLeaderData.Clear();
     mParent.Clear();
     mParentCandidate.Clear();
     ResetCounters();
@@ -142,9 +132,6 @@
     mMeshLocal16.GetAddress().GetIid().SetToLocator(0);
     mMeshLocal16.mRloc = true;
 
-    // Store RLOC address reference in MPL module.
-    Get<Ip6::Mpl>().SetMatchingAddress(mMeshLocal16.GetAddress());
-
     mLinkLocalAllThreadNodes.Clear();
     mLinkLocalAllThreadNodes.GetAddress().mFields.m16[0] = HostSwap16(0xff32);
     mLinkLocalAllThreadNodes.GetAddress().mFields.m16[7] = HostSwap16(0x0001);
@@ -168,7 +155,7 @@
     SuccessOrExit(error = mSocket.Bind(kUdpPort));
 
 #if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
-    StartParentSearchTimer();
+    mParentSearch.StartTimer();
 #endif
 exit:
     return error;
@@ -264,18 +251,54 @@
 exit:
     mDetachGracefullyTimer.Stop();
 
-    if (mDetachGracefullyCallback != nullptr)
+    if (mDetachGracefullyCallback.IsSet())
     {
-        otDetachGracefullyCallback callback = mDetachGracefullyCallback;
-        void *                     context  = mDetachGracefullyContext;
+        Callback<otDetachGracefullyCallback> callbackCopy = mDetachGracefullyCallback;
 
-        mDetachGracefullyCallback = nullptr;
-        mDetachGracefullyContext  = nullptr;
-
-        callback(context);
+        mDetachGracefullyCallback.Clear();
+        callbackCopy.Invoke();
     }
 }
 
+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)
+{
+    uint64_t currentUptimeMsec = Get<Uptime>().GetUptime();
+    uint64_t durationMsec      = currentUptimeMsec - mLastUpdatedTimestamp;
+
+    mLastUpdatedTimestamp = currentUptimeMsec;
+
+    mCounters.mTrackedTime += durationMsec;
+
+    switch (aRole)
+    {
+    case kRoleDisabled:
+        mCounters.mDisabledTime += durationMsec;
+        break;
+    case kRoleDetached:
+        mCounters.mDetachedTime += durationMsec;
+        break;
+    case kRoleChild:
+        mCounters.mChildTime += durationMsec;
+        break;
+    case kRoleRouter:
+        mCounters.mRouterTime += durationMsec;
+        break;
+    case kRoleLeader:
+        mCounters.mLeaderTime += durationMsec;
+        break;
+    }
+}
+#endif
+
 void Mle::SetRole(DeviceRole aRole)
 {
     DeviceRole oldRole = mRole;
@@ -284,6 +307,10 @@
 
     LogNote("Role %s -> %s", RoleToString(oldRole), RoleToString(mRole));
 
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    UpdateRoleTimeCounters(oldRole);
+#endif
+
     switch (mRole)
     {
     case kRoleDisabled:
@@ -309,6 +336,17 @@
         mParent.SetState(Neighbor::kStateInvalid);
     }
 
+    if ((oldRole == kRoleDetached) && IsChild())
+    {
+        // On transition from detached to child, we remember whether we
+        // attached as sleepy or not. This is then used to determine
+        // whether or not we need to re-attach on mode changes between
+        // rx-on and sleepy (rx-off). If we initially attach as sleepy,
+        // then rx-on/off mode changes are allowed without re-attach.
+
+        mInitiallyAttachedAsSleepy = !GetDeviceMode().IsRxOnWhenIdle();
+    }
+
 exit:
     return;
 }
@@ -339,7 +377,7 @@
 
     Get<KeyManager>().SetCurrentKeySequence(networkInfo.GetKeySequence());
     Get<KeyManager>().SetMleFrameCounter(networkInfo.GetMleFrameCounter());
-    Get<KeyManager>().SetAllMacFrameCounters(networkInfo.GetMacFrameCounter());
+    Get<KeyManager>().SetAllMacFrameCounters(networkInfo.GetMacFrameCounter(), /* aSetIfLarger */ false);
 
 #if OPENTHREAD_MTD
     mDeviceMode.Set(networkInfo.GetDeviceMode() & ~DeviceMode::kModeFullThreadDevice);
@@ -366,6 +404,7 @@
 #endif
     {
         Get<Mac::Mac>().SetShortAddress(networkInfo.GetRloc16());
+        mRloc16 = networkInfo.GetRloc16();
     }
     Get<Mac::Mac>().SetExtAddress(networkInfo.GetExtAddress());
 
@@ -393,7 +432,7 @@
 
         mParent.Clear();
         mParent.SetExtAddress(parentInfo.GetExtAddress());
-        mParent.SetVersion(static_cast<uint8_t>(parentInfo.GetVersion()));
+        mParent.SetVersion(parentInfo.GetVersion());
         mParent.SetDeviceMode(DeviceMode(DeviceMode::kModeFullThreadDevice | DeviceMode::kModeRxOnWhenIdle |
                                          DeviceMode::kModeFullNetworkData));
         mParent.SetRloc16(Rloc16FromRouterId(RouterIdFromRloc16(networkInfo.GetRloc16())));
@@ -408,6 +447,8 @@
         Get<MleRouter>().SetPreviousPartitionId(networkInfo.GetPreviousPartitionId());
         Get<ChildTable>().Restore();
     }
+
+    mWasLeader = networkInfo.GetRole() == kRoleLeader;
 #endif
 
     // Successfully restored the network information from non-volatile settings after boot.
@@ -462,10 +503,8 @@
     }
 
     networkInfo.SetKeySequence(Get<KeyManager>().GetCurrentKeySequence());
-    networkInfo.SetMleFrameCounter(Get<KeyManager>().GetMleFrameCounter() +
-                                   OPENTHREAD_CONFIG_STORE_FRAME_COUNTER_AHEAD);
-    networkInfo.SetMacFrameCounter(Get<KeyManager>().GetMaximumMacFrameCounter() +
-                                   OPENTHREAD_CONFIG_STORE_FRAME_COUNTER_AHEAD);
+    networkInfo.SetMleFrameCounter(Get<KeyManager>().GetMleFrameCounter() + kStoreFrameCounterAhead);
+    networkInfo.SetMacFrameCounter(Get<KeyManager>().GetMaximumMacFrameCounter() + kStoreFrameCounterAhead);
     networkInfo.SetDeviceMode(mDeviceMode.Get());
 
     SuccessOrExit(error = Get<Settings>().Save(networkInfo));
@@ -498,7 +537,7 @@
     }
 
 #if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
-    mParentSearchRecentlyDetached = true;
+    mParentSearch.SetRecentlyDetached();
 #endif
 
     SetStateDetached();
@@ -523,6 +562,17 @@
     return error;
 }
 
+Error Mle::SearchForBetterParent(void)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(IsChild(), error = kErrorInvalidState);
+    Attach(kBetterParent);
+
+exit:
+    return error;
+}
+
 void Mle::Attach(AttachMode aMode)
 {
     VerifyOrExit(!IsDisabled() && !IsAttaching());
@@ -622,22 +672,16 @@
         delay += jitter;
     }
 
-    LogNote("Attach attempt %d unsuccessful, will try again in %u.%03u seconds", mAttachCounter, delay / 1000,
-            delay % 1000);
+    LogNote("Attach attempt %u unsuccessful, will try again in %lu.%03u seconds", mAttachCounter, ToUlong(delay / 1000),
+            static_cast<uint16_t>(delay % 1000));
 
 exit:
     return delay;
 }
 
-bool Mle::IsAttached(void) const
-{
-    return (IsChild() || IsRouter() || IsLeader());
-}
+bool Mle::IsAttached(void) const { return (IsChild() || IsRouter() || IsLeader()); }
 
-bool Mle::IsRouterOrLeader(void) const
-{
-    return (IsRouter() || IsLeader());
-}
+bool Mle::IsRouterOrLeader(void) const { return (IsRouter() || IsLeader()); }
 
 void Mle::SetStateDetached(void)
 {
@@ -657,19 +701,16 @@
     SetAttachState(kAttachStateIdle);
     mAttachTimer.Stop();
     mMessageTransmissionTimer.Stop();
-    mChildUpdateRequestState = kChildUpdateRequestNone;
-    mChildUpdateAttempts     = 0;
-    mDataRequestState        = kDataRequestNone;
-    mDataRequestAttempts     = 0;
+    mChildUpdateRequestState   = kChildUpdateRequestNone;
+    mChildUpdateAttempts       = 0;
+    mDataRequestState          = kDataRequestNone;
+    mDataRequestAttempts       = 0;
+    mInitiallyAttachedAsSleepy = false;
     Get<MeshForwarder>().SetRxOnWhenIdle(true);
     Get<Mac::Mac>().SetBeaconEnabled(false);
 #if OPENTHREAD_FTD
     Get<MleRouter>().HandleDetachStart();
 #endif
-    Get<Ip6::Ip6>().SetForwardingEnabled(false);
-#if OPENTHREAD_FTD
-    Get<Ip6::Mpl>().SetTimerExpirations(0);
-#endif
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
     Get<Mac::Mac>().UpdateCsl();
 #endif
@@ -699,16 +740,11 @@
     }
 #endif
 
-    Get<Ip6::Ip6>().SetForwardingEnabled(false);
-#if OPENTHREAD_FTD
-    Get<Ip6::Mpl>().SetTimerExpirations(kMplChildDataMessageTimerExpirations);
-#endif
-
     // send announce after attached if needed
     InformPreviousChannel();
 
 #if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
-    UpdateParentSearchState();
+    mParentSearch.UpdateState();
 #endif
 
     if ((mPreviousParentRloc != Mac::kShortAddrInvalid) && (mPreviousParentRloc != mParent.GetRloc16()))
@@ -790,30 +826,46 @@
 
     IgnoreError(Store());
 
-    switch (mRole)
+    if (IsAttached())
     {
-    case kRoleDisabled:
-        break;
+        bool shouldReattach = false;
 
-    case kRoleDetached:
+        // We need to re-attach when switching between MTD/FTD modes.
+
+        if (oldMode.IsFullThreadDevice() != mDeviceMode.IsFullThreadDevice())
+        {
+            shouldReattach = true;
+        }
+
+        // If we initially attached as sleepy we allow mode changes
+        // between rx-on/off without a re-attach (we send "Child Update
+        // Request" to update the parent). But if we initially attached
+        // as rx-on, we require a re-attach on switching from rx-on to
+        // sleepy (rx-off) mode.
+
+        if (!mInitiallyAttachedAsSleepy && oldMode.IsRxOnWhenIdle() && !mDeviceMode.IsRxOnWhenIdle())
+        {
+            shouldReattach = true;
+        }
+
+        if (shouldReattach)
+        {
+            mAttachCounter = 0;
+            IgnoreError(BecomeDetached());
+            ExitNow();
+        }
+    }
+
+    if (IsDetached())
+    {
         mAttachCounter = 0;
         SetStateDetached();
         Attach(kAnyPartition);
-        break;
-
-    case kRoleChild:
+    }
+    else if (IsChild())
+    {
         SetStateChild(GetRloc16());
         IgnoreError(SendChildUpdateRequest());
-        break;
-
-    case kRoleRouter:
-    case kRoleLeader:
-        if (oldMode.IsFullThreadDevice() && !mDeviceMode.IsFullThreadDevice())
-        {
-            IgnoreError(BecomeDetached());
-        }
-
-        break;
     }
 
 exit:
@@ -936,11 +988,6 @@
     Get<Notifier>().Signal(kEventThreadMeshLocalAddrChanged);
 }
 
-uint16_t Mle::GetRloc16(void) const
-{
-    return Get<Mac::Mac>().GetShortAddress();
-}
-
 void Mle::SetRloc16(uint16_t aRloc16)
 {
     uint16_t oldRloc16 = GetRloc16();
@@ -958,7 +1005,7 @@
     }
 
     Get<Mac::Mac>().SetShortAddress(aRloc16);
-    Get<Ip6::Mpl>().SetSeedId(aRloc16);
+    mRloc16 = aRloc16;
 
     if (aRloc16 != Mac::kShortAddrInvalid)
     {
@@ -1084,6 +1131,15 @@
 }
 #endif
 
+void Mle::InitNeighbor(Neighbor &aNeighbor, const RxInfo &aRxInfo)
+{
+    aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(aNeighbor.GetExtAddress());
+    aNeighbor.GetLinkInfo().Clear();
+    aNeighbor.GetLinkInfo().AddRss(aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss());
+    aNeighbor.ResetLinkFailures();
+    aNeighbor.SetLastHeard(TimerMilli::GetNow());
+}
+
 void Mle::HandleNotifierEvents(Events aEvents)
 {
     VerifyOrExit(!IsDisabled());
@@ -1277,11 +1333,6 @@
 
 #endif // OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
 
-void Mle::HandleAttachTimer(Timer &aTimer)
-{
-    aTimer.Get<Mle>().HandleAttachTimer();
-}
-
 Error Mle::DetermineParentRequestType(ParentRequestType &aType) const
 {
     // This method determines the Parent Request type to use during an
@@ -1338,7 +1389,6 @@
 bool Mle::HasAcceptableParentCandidate(void) const
 {
     bool              hasAcceptableParent = false;
-    LinkQuality       linkQuality;
     ParentRequestType parentReqType;
 
     VerifyOrExit(mParentCandidate.IsStateParentResponse());
@@ -1346,7 +1396,7 @@
     switch (mAttachState)
     {
     case kAttachStateAnnounce:
-        VerifyOrExit(!HasMoreChannelsToAnnouce());
+        VerifyOrExit(!HasMoreChannelsToAnnounce());
         break;
 
     case kAttachStateParentRequest:
@@ -1358,8 +1408,7 @@
             // in Parent Request was sent to routers, we will keep the
             // candidate and forward to REED stage to potentially find a
             // better parent.
-            linkQuality = OT_MIN(mParentCandidate.GetLinkInfo().GetLinkQuality(), mParentCandidate.GetLinkQualityOut());
-            VerifyOrExit(linkQuality == kLinkQuality3);
+            VerifyOrExit(mParentCandidate.GetTwoWayLinkQuality() == kLinkQuality3);
         }
 
         break;
@@ -1398,7 +1447,7 @@
     if (HasAcceptableParentCandidate() && (SendChildIdRequest() == kErrorNone))
     {
         SetAttachState(kAttachStateChildIdRequest);
-        delay = kParentRequestReedTimeout;
+        delay = kChildIdResponseTimeout;
         ExitNow();
     }
 
@@ -1455,7 +1504,7 @@
         OT_FALL_THROUGH;
 
     case kAttachStateAnnounce:
-        if (shouldAnnounce && (GetNextAnnouceChannel(mAnnounceChannel) == kErrorNone))
+        if (shouldAnnounce && (GetNextAnnounceChannel(mAnnounceChannel) == kErrorNone))
         {
             SendAnnounce(mAnnounceChannel, kOrphanAnnounce);
             delay = mAnnounceDelay;
@@ -1581,11 +1630,6 @@
     return delay;
 }
 
-void Mle::HandleDelayedResponseTimer(Timer &aTimer)
-{
-    aTimer.Get<Mle>().HandleDelayedResponseTimer();
-}
-
 void Mle::HandleDelayedResponseTimer(void)
 {
     TimeMilli now          = TimerMilli::GetNow();
@@ -1599,10 +1643,7 @@
 
         if (now < metadata.mSendTime)
         {
-            if (nextSendTime > metadata.mSendTime)
-            {
-                nextSendTime = metadata.mSendTime;
-            }
+            nextSendTime = Min(nextSendTime, metadata.mSendTime);
         }
         else
         {
@@ -1643,7 +1684,11 @@
     }
 
 exit:
-    FreeMessageOnError(&aMessage, error);
+    if (error != kErrorNone)
+    {
+        // do not use `FreeMessageOnError()` to avoid null check on nonnull pointer
+        aMessage.Free();
+    }
 }
 
 void Mle::RemoveDelayedDataResponseMessage(void)
@@ -1676,7 +1721,7 @@
 void Mle::SendParentRequest(ParentRequestType aType)
 {
     Error        error = kErrorNone;
-    TxMessage *  message;
+    TxMessage   *message;
     uint8_t      scanMask = 0;
     Ip6::Address destination;
 
@@ -1731,10 +1776,11 @@
 
 Error Mle::SendChildIdRequest(void)
 {
+    static const uint8_t kTlvs[] = {Tlv::kAddress16, Tlv::kNetworkData, Tlv::kRoute};
+
     Error        error   = kErrorNone;
-    uint8_t      tlvs[]  = {Tlv::kAddress16, Tlv::kNetworkData, Tlv::kRoute};
-    uint8_t      tlvsLen = sizeof(tlvs);
-    TxMessage *  message = nullptr;
+    uint8_t      tlvsLen = sizeof(kTlvs);
+    TxMessage   *message = nullptr;
     Ip6::Address destination;
 
     if (mParent.GetExtAddress() == mParentCandidate.GetExtAddress())
@@ -1756,12 +1802,13 @@
     }
 
     VerifyOrExit((message = NewMleMessage(kCommandChildIdRequest)) != nullptr, error = kErrorNoBufs);
-    SuccessOrExit(error = message->AppendResponseTlv(mParentCandidateChallenge));
+    SuccessOrExit(error = message->AppendResponseTlv(mParentCandidate.mChallenge));
     SuccessOrExit(error = message->AppendLinkFrameCounterTlv());
     SuccessOrExit(error = message->AppendMleFrameCounterTlv());
     SuccessOrExit(error = message->AppendModeTlv(mDeviceMode));
     SuccessOrExit(error = message->AppendTimeoutTlv(mTimeout));
     SuccessOrExit(error = message->AppendVersionTlv());
+    SuccessOrExit(error = message->AppendSupervisionIntervalTlv(Get<SupervisionListener>().GetInterval()));
 
     if (!IsFullThreadDevice())
     {
@@ -1771,7 +1818,7 @@
         tlvsLen -= 1;
     }
 
-    SuccessOrExit(error = message->AppendTlvRequestTlv(tlvs, tlvsLen));
+    SuccessOrExit(error = message->AppendTlvRequestTlv(kTlvs, tlvsLen));
     SuccessOrExit(error = message->AppendActiveTimestampTlv());
     SuccessOrExit(error = message->AppendPendingTimestampTlv());
 
@@ -1795,12 +1842,38 @@
     return error;
 }
 
-Error Mle::SendDataRequest(const Ip6::Address &aDestination,
-                           const uint8_t *     aTlvs,
-                           uint8_t             aTlvsLength,
-                           uint16_t            aDelay,
-                           const uint8_t *     aExtraTlvs,
-                           uint8_t             aExtraTlvsLength)
+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
 {
     Error      error = kErrorNone;
     TxMessage *message;
@@ -1810,10 +1883,12 @@
     VerifyOrExit((message = NewMleMessage(kCommandDataRequest)) != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = message->AppendTlvRequestTlv(aTlvs, aTlvsLength));
 
-    if (aExtraTlvs != nullptr && aExtraTlvsLength > 0)
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    if (aQueryInfo != nullptr)
     {
-        SuccessOrExit(error = message->AppendBytes(aExtraTlvs, aExtraTlvsLength));
+        SuccessOrExit(error = Get<LinkMetrics::Initiator>().AppendLinkMetricsQueryTlv(*message, *aQueryInfo));
     }
+#endif
 
     if (aDelay)
     {
@@ -1854,6 +1929,14 @@
 {
     uint32_t interval = 0;
 
+#if OPENTHREAD_FTD
+    if (mRole == kRoleDetached && mLinkRequestAttempts > 0)
+    {
+        ExitNow(interval = Random::NonCrypto::GetUint32InRange(kMulticastTransmissionDelayMin,
+                                                               kMulticastTransmissionDelayMax));
+    }
+#endif
+
     switch (mChildUpdateRequestState)
     {
     case kChildUpdateRequestNone:
@@ -1866,8 +1949,7 @@
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
         if (Get<Mac::Mac>().IsCslEnabled())
         {
-            ExitNow(interval = Get<Mac::Mac>().GetCslPeriod() * kUsPerTenSymbols / 1000 +
-                               static_cast<uint32_t>(kUnicastRetransmissionDelay));
+            ExitNow(interval = Get<Mac::Mac>().GetCslPeriodMs() + static_cast<uint32_t>(kUnicastRetransmissionDelay));
         }
         else
 #endif
@@ -1902,11 +1984,6 @@
     }
 }
 
-void Mle::HandleMessageTransmissionTimer(Timer &aTimer)
-{
-    aTimer.Get<Mle>().HandleMessageTransmissionTimer();
-}
-
 void Mle::HandleMessageTransmissionTimer(void)
 {
     // The `mMessageTransmissionTimer` is used for:
@@ -1915,20 +1992,32 @@
     //  - Retransmission of "Child Update Request",
     //  - Retransmission of "Data Request" on a child,
     //  - Sending periodic keep-alive "Child Update Request" messages on a non-sleepy (rx-on) child.
+    //  - Retransmission of "Link Request" after router reset
+
+#if OPENTHREAD_FTD
+    // Retransmit multicast link request if no response has been received
+    // and maximum transmission limit has not been reached.
+    if (mRole == kRoleDetached && mLinkRequestAttempts > 0)
+    {
+        IgnoreError(Get<MleRouter>().SendLinkRequest(nullptr));
+        mLinkRequestAttempts--;
+        ScheduleMessageTransmissionTimer();
+        ExitNow();
+    }
+#endif
 
     switch (mChildUpdateRequestState)
     {
     case kChildUpdateRequestNone:
         if (mDataRequestState == kDataRequestActive)
         {
-            static const uint8_t tlvs[] = {Tlv::kNetworkData};
-            Ip6::Address         destination;
+            Ip6::Address destination;
 
             VerifyOrExit(mDataRequestAttempts < kMaxChildKeepAliveAttempts, IgnoreError(BecomeDetached()));
 
             destination.SetToLinkLocalAddress(mParent.GetExtAddress());
 
-            if (SendDataRequest(destination, tlvs, sizeof(tlvs), 0) == kErrorNone)
+            if (SendDataRequest(destination) == kErrorNone)
             {
                 mDataRequestAttempts++;
             }
@@ -1968,17 +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())
     {
@@ -1993,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));
@@ -2002,13 +2088,14 @@
     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())
         {
@@ -2022,12 +2109,11 @@
     case kRoleRouter:
     case kRoleLeader:
         OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
     }
 
     if (!IsFullThreadDevice())
     {
-        SuccessOrExit(error = message->AppendAddressRegistrationTlv(mode));
+        SuccessOrExit(error = message->AppendAddressRegistrationTlv(addrRegMode));
     }
 
     destination.SetToLinkLocalAddress(mParent.GetExtAddress());
@@ -2054,20 +2140,20 @@
     return error;
 }
 
-Error Mle::SendChildUpdateResponse(const uint8_t *aTlvs, uint8_t aNumTlvs, const Challenge &aChallenge)
+Error Mle::SendChildUpdateResponse(const TlvList &aTlvList, const Challenge &aChallenge)
 {
     Error        error = kErrorNone;
     Ip6::Address destination;
-    TxMessage *  message;
+    TxMessage   *message;
     bool         checkAddress = false;
 
     VerifyOrExit((message = NewMleMessage(kCommandChildUpdateResponse)) != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = message->AppendSourceAddressTlv());
     SuccessOrExit(error = message->AppendLeaderDataTlv());
 
-    for (int i = 0; i < aNumTlvs; i++)
+    for (uint8_t tlvType : aTlvList)
     {
-        switch (aTlvs[i])
+        switch (tlvType)
         {
         case Tlv::kTimeout:
             SuccessOrExit(error = message->AppendTimeoutTlv(mTimeout));
@@ -2103,6 +2189,10 @@
             SuccessOrExit(error = message->AppendMleFrameCounterTlv());
             break;
 
+        case Tlv::kSupervisionInterval:
+            SuccessOrExit(error = message->AppendSupervisionIntervalTlv(Get<SupervisionListener>().GetInterval()));
+            break;
+
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
         case Tlv::kCslTimeout:
             if (Get<Mac::Mac>().IsCslEnabled())
@@ -2141,19 +2231,19 @@
 void Mle::SendAnnounce(uint8_t aChannel, const Ip6::Address &aDestination, AnnounceMode aMode)
 {
     Error              error = kErrorNone;
-    ChannelTlv         channel;
+    ChannelTlv         channelTlv;
     MeshCoP::Timestamp activeTimestamp;
-    TxMessage *        message = nullptr;
+    TxMessage         *message = nullptr;
 
     VerifyOrExit(Get<Mac::Mac>().GetSupportedChannelMask().ContainsChannel(aChannel), error = kErrorInvalidArgs);
     VerifyOrExit((message = NewMleMessage(kCommandAnnounce)) != nullptr, error = kErrorNoBufs);
     message->SetLinkSecurityEnabled(true);
     message->SetChannel(aChannel);
 
-    channel.Init();
-    channel.SetChannelPage(0);
-    channel.SetChannel(Get<Mac::Mac>().GetPanChannel());
-    SuccessOrExit(error = channel.AppendTo(*message));
+    channelTlv.Init();
+    channelTlv.SetChannelPage(0);
+    channelTlv.SetChannel(Get<Mac::Mac>().GetPanChannel());
+    SuccessOrExit(error = channelTlv.AppendTo(*message));
 
     switch (aMode)
     {
@@ -2178,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
@@ -2194,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
@@ -2253,10 +2343,10 @@
 #endif
 
 Error Mle::ProcessMessageSecurity(Crypto::AesCcm::Mode    aMode,
-                                  Message &               aMessage,
+                                  Message                &aMessage,
                                   const Ip6::MessageInfo &aMessageInfo,
                                   uint16_t                aCmdOffset,
-                                  const SecurityHeader &  aHeader)
+                                  const SecurityHeader   &aHeader)
 {
     // This method performs MLE message security. Based on `aMode` it
     // can be used to encrypt and append tag to `aMessage` or to
@@ -2303,7 +2393,7 @@
     }
 
     senderAddress->GetIid().ConvertToExtAddress(extAddress);
-    Crypto::AesCcm::GenerateNonce(extAddress, aHeader.GetFrameCounter(), Mac::Frame::kSecEncMic32, nonce);
+    Crypto::AesCcm::GenerateNonce(extAddress, aHeader.GetFrameCounter(), Mac::Frame::kSecurityEncMic32, nonce);
 
     keySequence = aHeader.GetKeyId();
 
@@ -2359,7 +2449,7 @@
     uint32_t        frameCounter;
     Mac::ExtAddress extAddr;
     uint8_t         command;
-    Neighbor *      neighbor;
+    Neighbor       *neighbor;
 
     LogDebg("Receive MLE message");
 
@@ -2647,7 +2737,7 @@
 
     if (IsChild() && (&aNeighbor == &mParent))
     {
-        IgnoreError(SendChildUpdateRequest(/* aAppendChallenge */ true));
+        IgnoreError(SendChildUpdateRequest(kAppendChallengeTlv));
         ExitNow();
     }
 
@@ -2676,79 +2766,57 @@
     Error      error = kErrorNone;
     uint16_t   sourceAddress;
     LeaderData leaderData;
-    uint8_t    tlvs[] = {Tlv::kNetworkData};
     uint16_t   delay;
 
-    // Source Address
+    VerifyOrExit(IsAttached());
+
     SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));
 
     Log(kMessageReceive, kTypeAdvertisement, aRxInfo.mMessageInfo.GetPeerAddr(), sourceAddress);
 
-    // Leader Data
     SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
 
-    if (!IsDetached())
-    {
 #if OPENTHREAD_FTD
-        if (IsFullThreadDevice())
-        {
-            SuccessOrExit(error = Get<MleRouter>().HandleAdvertisement(aRxInfo));
-        }
-        else
-#endif
-        {
-            if ((aRxInfo.mNeighbor == &mParent) && (mParent.GetRloc16() != sourceAddress))
-            {
-                // Remove stale parent.
-                IgnoreError(BecomeDetached());
-            }
-        }
-    }
-
-    switch (mRole)
+    if (IsFullThreadDevice())
     {
-    case kRoleDisabled:
-    case kRoleDetached:
-        ExitNow();
+        SuccessOrExit(error = Get<MleRouter>().HandleAdvertisement(aRxInfo, sourceAddress, leaderData));
+    }
+#endif
 
-    case kRoleChild:
+    if (IsChild())
+    {
         VerifyOrExit(aRxInfo.mNeighbor == &mParent);
 
-        if ((mParent.GetRloc16() == sourceAddress) && (leaderData.GetPartitionId() != mLeaderData.GetPartitionId() ||
-                                                       leaderData.GetLeaderRouterId() != GetLeaderId()))
+        if (mParent.GetRloc16() != sourceAddress)
+        {
+            // Remove stale parent.
+            IgnoreError(BecomeDetached());
+            ExitNow();
+        }
+
+        if ((leaderData.GetPartitionId() != mLeaderData.GetPartitionId()) ||
+            (leaderData.GetLeaderRouterId() != GetLeaderId()))
         {
             SetLeaderData(leaderData.GetPartitionId(), leaderData.GetWeighting(), leaderData.GetLeaderRouterId());
 
 #if OPENTHREAD_FTD
-            if (IsFullThreadDevice())
-            {
-                switch (Get<MleRouter>().ProcessRouteTlv(aRxInfo))
-                {
-                case kErrorNone:
-                case kErrorNotFound:
-                    break;
-                default:
-                    ExitNow(error = kErrorParse);
-                }
-            }
+            SuccessOrExit(error = Get<MleRouter>().ReadAndProcessRouteTlvOnFed(aRxInfo, mParent.GetRouterId()));
 #endif
 
             mRetrieveNewNetworkData = true;
         }
 
         mParent.SetLastHeard(TimerMilli::GetNow());
-        break;
-
-    case kRoleRouter:
-    case kRoleLeader:
-        VerifyOrExit(aRxInfo.mNeighbor && aRxInfo.mNeighbor->IsStateValid());
-        break;
+    }
+    else // Device is router or leader
+    {
+        VerifyOrExit(aRxInfo.IsNeighborStateValid());
     }
 
     if (mRetrieveNewNetworkData || IsNetworkDataNewer(leaderData))
     {
         delay = Random::NonCrypto::GetUint16InRange(0, kMleMaxResponseDelay);
-        IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr(), tlvs, sizeof(tlvs), delay));
+        IgnoreError(SendDataRequestAfterDelay(aRxInfo.mMessageInfo.GetPeerAddr(), delay));
     }
 
     aRxInfo.mClass = RxInfo::kPeerMessage;
@@ -2760,24 +2828,28 @@
 void Mle::HandleDataResponse(RxInfo &aRxInfo)
 {
     Error error;
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
-    uint16_t metricsReportValueOffset;
-    uint16_t length;
-#endif
 
     Log(kMessageReceive, kTypeDataResponse, aRxInfo.mMessageInfo.GetPeerAddr());
 
-    VerifyOrExit(aRxInfo.mNeighbor && aRxInfo.mNeighbor->IsStateValid(), error = kErrorDrop);
+    VerifyOrExit(aRxInfo.IsNeighborStateValid(), error = kErrorDrop);
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
-    if (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kLinkMetricsReport, metricsReportValueOffset, length) ==
-        kErrorNone)
     {
-        Get<LinkMetrics::LinkMetrics>().HandleReport(aRxInfo.mMessage, metricsReportValueOffset, length,
-                                                     aRxInfo.mMessageInfo.GetPeerAddr());
+        uint16_t offset;
+        uint16_t length;
+
+        if (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kLinkMetricsReport, offset, length) == kErrorNone)
+        {
+            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())
@@ -2810,13 +2882,15 @@
     MeshCoP::Timestamp        activeTimestamp;
     MeshCoP::Timestamp        pendingTimestamp;
     const MeshCoP::Timestamp *timestamp;
-    bool                      hasActiveTimestamp   = false;
-    bool                      hasPendingTimestamp  = false;
-    uint16_t                  networkDataOffset    = 0;
+    bool                      hasActiveTimestamp  = false;
+    bool                      hasPendingTimestamp = false;
+    uint16_t                  networkDataOffset;
+    uint16_t                  networkDataLength;
     uint16_t                  activeDatasetOffset  = 0;
+    uint16_t                  activeDatasetLength  = 0;
     uint16_t                  pendingDatasetOffset = 0;
+    uint16_t                  pendingDatasetLength = 0;
     bool                      dataRequest          = false;
-    Tlv                       tlv;
 
     // Leader Data
     SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
@@ -2850,7 +2924,8 @@
         // if received timestamp does not match the local value and message does not contain the dataset,
         // send MLE Data Request
         if (!IsLeader() && (MeshCoP::Timestamp::Compare(&activeTimestamp, timestamp) != 0) &&
-            (Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kActiveDataset, activeDatasetOffset) != kErrorNone))
+            (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kActiveDataset, activeDatasetOffset, activeDatasetLength) !=
+             kErrorNone))
         {
             ExitNow(dataRequest = true);
         }
@@ -2875,7 +2950,8 @@
         // if received timestamp does not match the local value and message does not contain the dataset,
         // send MLE Data Request
         if (!IsLeader() && (MeshCoP::Timestamp::Compare(&pendingTimestamp, timestamp) != 0) &&
-            (Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kPendingDataset, pendingDatasetOffset) != kErrorNone))
+            (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kPendingDataset, pendingDatasetOffset,
+                                     pendingDatasetLength) != kErrorNone))
         {
             ExitNow(dataRequest = true);
         }
@@ -2889,11 +2965,12 @@
         ExitNow(error = kErrorParse);
     }
 
-    if (Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kNetworkData, networkDataOffset) == kErrorNone)
+    if (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kNetworkData, networkDataOffset, networkDataLength) ==
+        kErrorNone)
     {
-        error = Get<NetworkData::Leader>().SetNetworkData(leaderData.GetDataVersion(NetworkData::kFullSet),
-                                                          leaderData.GetDataVersion(NetworkData::kStableSubset),
-                                                          GetNetworkDataType(), aRxInfo.mMessage, networkDataOffset);
+        error = Get<NetworkData::Leader>().SetNetworkData(
+            leaderData.GetDataVersion(NetworkData::kFullSet), leaderData.GetDataVersion(NetworkData::kStableSubset),
+            GetNetworkDataType(), aRxInfo.mMessage, networkDataOffset, networkDataLength);
         SuccessOrExit(error);
     }
     else
@@ -2914,9 +2991,8 @@
         {
             if (activeDatasetOffset > 0)
             {
-                IgnoreError(aRxInfo.mMessage.Read(activeDatasetOffset, tlv));
-                IgnoreError(Get<MeshCoP::ActiveDatasetManager>().Save(
-                    activeTimestamp, aRxInfo.mMessage, activeDatasetOffset + sizeof(tlv), tlv.GetLength()));
+                IgnoreError(Get<MeshCoP::ActiveDatasetManager>().Save(activeTimestamp, aRxInfo.mMessage,
+                                                                      activeDatasetOffset, activeDatasetLength));
             }
         }
 
@@ -2925,9 +3001,8 @@
         {
             if (pendingDatasetOffset > 0)
             {
-                IgnoreError(aRxInfo.mMessage.Read(pendingDatasetOffset, tlv));
-                IgnoreError(Get<MeshCoP::PendingDatasetManager>().Save(
-                    pendingTimestamp, aRxInfo.mMessage, pendingDatasetOffset + sizeof(tlv), tlv.GetLength()));
+                IgnoreError(Get<MeshCoP::PendingDatasetManager>().Save(pendingTimestamp, aRxInfo.mMessage,
+                                                                       pendingDatasetOffset, pendingDatasetLength));
             }
         }
     }
@@ -2938,8 +3013,7 @@
 
     if (dataRequest)
     {
-        static const uint8_t tlvs[] = {Tlv::kNetworkData};
-        uint16_t             delay;
+        uint16_t delay;
 
         if (aRxInfo.mMessageInfo.GetSockAddr().IsMulticast())
         {
@@ -2953,7 +3027,7 @@
             delay = 10;
         }
 
-        IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr(), tlvs, sizeof(tlvs), delay));
+        IgnoreError(SendDataRequestAfterDelay(aRxInfo.mMessageInfo.GetPeerAddr(), delay));
     }
     else if (error == kErrorNone)
     {
@@ -2971,115 +3045,87 @@
     return error;
 }
 
-bool Mle::IsBetterParent(uint16_t               aRloc16,
-                         LinkQuality            aLinkQuality,
-                         uint8_t                aLinkMargin,
-                         const ConnectivityTlv &aConnectivityTlv,
-                         uint8_t                aVersion,
-                         uint8_t                aCslClockAccuracy,
-                         uint8_t                aCslUncertainty)
+bool Mle::IsBetterParent(uint16_t                aRloc16,
+                         LinkQuality             aLinkQuality,
+                         uint8_t                 aLinkMargin,
+                         const ConnectivityTlv  &aConnectivityTlv,
+                         uint16_t                aVersion,
+                         const Mac::CslAccuracy &aCslAccuracy)
 {
-    bool rval = false;
-
-    LinkQuality candidateLinkQualityIn     = mParentCandidate.GetLinkInfo().GetLinkQuality();
-    LinkQuality candidateTwoWayLinkQuality = OT_MIN(candidateLinkQualityIn, mParentCandidate.GetLinkQualityOut());
-#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    uint64_t candidateCslMetric = 0;
-    uint64_t cslMetric          = 0;
-#else
-    OT_UNUSED_VARIABLE(aCslClockAccuracy);
-    OT_UNUSED_VARIABLE(aCslUncertainty);
-#endif
+    int         rval;
+    LinkQuality candidateTwoWayLinkQuality = mParentCandidate.GetTwoWayLinkQuality();
 
     // Mesh Impacting Criteria
-    if (aLinkQuality != candidateTwoWayLinkQuality)
-    {
-        ExitNow(rval = (aLinkQuality > candidateTwoWayLinkQuality));
-    }
+    rval = ThreeWayCompare(aLinkQuality, candidateTwoWayLinkQuality);
+    VerifyOrExit(rval == 0);
 
-    if (IsActiveRouter(aRloc16) != IsActiveRouter(mParentCandidate.GetRloc16()))
-    {
-        ExitNow(rval = IsActiveRouter(aRloc16));
-    }
+    rval = ThreeWayCompare(IsActiveRouter(aRloc16), IsActiveRouter(mParentCandidate.GetRloc16()));
+    VerifyOrExit(rval == 0);
 
-    if (aConnectivityTlv.GetParentPriority() != mParentPriority)
-    {
-        ExitNow(rval = (aConnectivityTlv.GetParentPriority() > mParentPriority));
-    }
+    rval = ThreeWayCompare(aConnectivityTlv.GetParentPriority(), mParentCandidate.mPriority);
+    VerifyOrExit(rval == 0);
 
     // Prefer the parent with highest quality links (Link Quality 3 field in Connectivity TLV) to neighbors
-    if (aConnectivityTlv.GetLinkQuality3() != mParentLinkQuality3)
-    {
-        ExitNow(rval = (aConnectivityTlv.GetLinkQuality3() > mParentLinkQuality3));
-    }
+    rval = ThreeWayCompare(aConnectivityTlv.GetLinkQuality3(), mParentCandidate.mLinkQuality3);
+    VerifyOrExit(rval == 0);
 
     // Thread 1.2 Specification 4.5.2.1.2 Child Impacting Criteria
-    if (aVersion != mParentCandidate.GetVersion())
-    {
-        ExitNow(rval = (aVersion > mParentCandidate.GetVersion()));
-    }
 
-    if (aConnectivityTlv.GetSedBufferSize() != mParentSedBufferSize)
-    {
-        ExitNow(rval = (aConnectivityTlv.GetSedBufferSize() > mParentSedBufferSize));
-    }
+    rval = ThreeWayCompare(aVersion, mParentCandidate.GetVersion());
+    VerifyOrExit(rval == 0);
 
-    if (aConnectivityTlv.GetSedDatagramCount() != mParentSedDatagramCount)
-    {
-        ExitNow(rval = (aConnectivityTlv.GetSedDatagramCount() > mParentSedDatagramCount));
-    }
+    rval = ThreeWayCompare(aConnectivityTlv.GetSedBufferSize(), mParentCandidate.mSedBufferSize);
+    VerifyOrExit(rval == 0);
+
+    rval = ThreeWayCompare(aConnectivityTlv.GetSedDatagramCount(), mParentCandidate.mSedDatagramCount);
+    VerifyOrExit(rval == 0);
 
     // Extra rules
-    if (aConnectivityTlv.GetLinkQuality2() != mParentLinkQuality2)
-    {
-        ExitNow(rval = (aConnectivityTlv.GetLinkQuality2() > mParentLinkQuality2));
-    }
+    rval = ThreeWayCompare(aConnectivityTlv.GetLinkQuality2(), mParentCandidate.mLinkQuality2);
+    VerifyOrExit(rval == 0);
 
-    if (aConnectivityTlv.GetLinkQuality1() != mParentLinkQuality1)
-    {
-        ExitNow(rval = (aConnectivityTlv.GetLinkQuality1() > mParentLinkQuality1));
-    }
+    rval = ThreeWayCompare(aConnectivityTlv.GetLinkQuality1(), mParentCandidate.mLinkQuality1);
+    VerifyOrExit(rval == 0);
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
     // CSL metric
     if (!IsRxOnWhenIdle())
     {
-        cslMetric = CalcParentCslMetric(aCslClockAccuracy, aCslUncertainty);
-        candidateCslMetric =
-            CalcParentCslMetric(mParentCandidate.GetCslClockAccuracy(), mParentCandidate.GetCslUncertainty());
-        if (candidateCslMetric != cslMetric)
-        {
-            ExitNow(rval = (cslMetric < candidateCslMetric));
-        }
+        uint64_t cslMetric          = CalcParentCslMetric(aCslAccuracy);
+        uint64_t candidateCslMetric = CalcParentCslMetric(mParentCandidate.GetCslAccuracy());
+
+        // Smaller metric is better.
+        rval = ThreeWayCompare(candidateCslMetric, cslMetric);
+        VerifyOrExit(rval == 0);
     }
+#else
+    OT_UNUSED_VARIABLE(aCslAccuracy);
 #endif
 
-    rval = (aLinkMargin > mParentLinkMargin);
+    rval = ThreeWayCompare(aLinkMargin, mParentCandidate.mLinkMargin);
 
 exit:
-    return rval;
+    return (rval > 0);
 }
 
 void Mle::HandleParentResponse(RxInfo &aRxInfo)
 {
-    Error                 error    = kErrorNone;
-    const ThreadLinkInfo *linkInfo = aRxInfo.mMessageInfo.GetThreadLinkInfo();
-    Challenge             response;
-    uint16_t              version;
-    uint16_t              sourceAddress;
-    LeaderData            leaderData;
-    uint8_t               linkMarginFromTlv;
-    uint8_t               linkMargin;
-    LinkQuality           linkQuality;
-    ConnectivityTlv       connectivity;
-    uint32_t              linkFrameCounter;
-    uint32_t              mleFrameCounter;
-    Mac::ExtAddress       extAddress;
+    Error            error = kErrorNone;
+    int8_t           rss   = aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss();
+    Challenge        response;
+    uint16_t         version;
+    uint16_t         sourceAddress;
+    LeaderData       leaderData;
+    uint8_t          linkMarginFromTlv;
+    uint8_t          linkMargin;
+    LinkQuality      linkQuality;
+    ConnectivityTlv  connectivityTlv;
+    uint32_t         linkFrameCounter;
+    uint32_t         mleFrameCounter;
+    Mac::ExtAddress  extAddress;
+    Mac::CslAccuracy cslAccuracy;
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
-    TimeParameterTlv timeParameter;
-#endif
-#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    CslClockAccuracyTlv clockAccuracy;
+    TimeParameterTlv timeParameterTlv;
 #endif
 
     // Source Address
@@ -3089,7 +3135,7 @@
 
     // Version
     SuccessOrExit(error = Tlv::Find<VersionTlv>(aRxInfo.mMessage, version));
-    VerifyOrExit(version >= OT_THREAD_VERSION_1_1, error = kErrorParse);
+    VerifyOrExit(version >= kThreadVersion1p1, error = kErrorParse);
 
     // Response
     SuccessOrExit(error = aRxInfo.mMessage.ReadResponseTlv(response));
@@ -3108,44 +3154,52 @@
     // Link Margin
     SuccessOrExit(error = Tlv::Find<LinkMarginTlv>(aRxInfo.mMessage, linkMarginFromTlv));
 
-    linkMargin = LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(), linkInfo->GetRss());
+    linkMargin = Get<Mac::Mac>().ComputeLinkMargin(rss);
 
     if (linkMargin > linkMarginFromTlv)
     {
         linkMargin = linkMarginFromTlv;
     }
 
-    linkQuality = LinkQualityInfo::ConvertLinkMarginToLinkQuality(linkMargin);
+    linkQuality = LinkQualityForLinkMargin(linkMargin);
 
     // Connectivity
-    SuccessOrExit(error = Tlv::FindTlv(aRxInfo.mMessage, connectivity));
-    VerifyOrExit(connectivity.IsValid(), error = kErrorParse);
+    SuccessOrExit(error = Tlv::FindTlv(aRxInfo.mMessage, connectivityTlv));
+    VerifyOrExit(connectivityTlv.IsValid(), error = kErrorParse);
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
     // CSL Accuracy
-    if (Tlv::FindTlv(aRxInfo.mMessage, clockAccuracy) != kErrorNone)
+    switch (aRxInfo.mMessage.ReadCslClockAccuracyTlv(cslAccuracy))
     {
-        clockAccuracy.SetCslClockAccuracy(kCslWorstCrystalPpm);
-        clockAccuracy.SetCslUncertainty(kCslWorstUncertainty);
+    case kErrorNone:
+        break;
+    case kErrorNotFound:
+        cslAccuracy.Init(); // Use worst-case values if TLV is not found
+        break;
+    default:
+        ExitNow(error = kErrorParse);
     }
+#else
+    cslAccuracy.Init();
 #endif
 
-    // Share data with application, if requested.
-    if (mParentResponseCb)
+#if OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
+    if (mParentResponseCallback.IsSet())
     {
         otThreadParentResponseInfo parentinfo;
 
         parentinfo.mExtAddr      = extAddress;
         parentinfo.mRloc16       = sourceAddress;
-        parentinfo.mRssi         = linkInfo->GetRss();
-        parentinfo.mPriority     = connectivity.GetParentPriority();
-        parentinfo.mLinkQuality3 = connectivity.GetLinkQuality3();
-        parentinfo.mLinkQuality2 = connectivity.GetLinkQuality2();
-        parentinfo.mLinkQuality1 = connectivity.GetLinkQuality1();
+        parentinfo.mRssi         = rss;
+        parentinfo.mPriority     = connectivityTlv.GetParentPriority();
+        parentinfo.mLinkQuality3 = connectivityTlv.GetLinkQuality3();
+        parentinfo.mLinkQuality2 = connectivityTlv.GetLinkQuality2();
+        parentinfo.mLinkQuality1 = connectivityTlv.GetLinkQuality1();
         parentinfo.mIsAttached   = IsAttached();
 
-        mParentResponseCb(&parentinfo, mParentResponseCbContext);
+        mParentResponseCallback.Invoke(&parentinfo);
     }
+#endif
 
     aRxInfo.mClass = RxInfo::kAuthoritativeMessage;
 
@@ -3153,9 +3207,9 @@
     if (IsFullThreadDevice() && !IsDetached())
     {
         bool isPartitionIdSame = (leaderData.GetPartitionId() == mLeaderData.GetPartitionId());
-        bool isIdSequenceSame  = (connectivity.GetIdSequence() == Get<RouterTable>().GetRouterIdSequence());
+        bool isIdSequenceSame  = (connectivityTlv.GetIdSequence() == Get<RouterTable>().GetRouterIdSequence());
         bool isIdSequenceGreater =
-            SerialNumber::IsGreater(connectivity.GetIdSequence(), Get<RouterTable>().GetRouterIdSequence());
+            SerialNumber::IsGreater(connectivityTlv.GetIdSequence(), Get<RouterTable>().GetRouterIdSequence());
 
         switch (mAttachMode)
         {
@@ -3176,7 +3230,7 @@
         case kBetterPartition:
             VerifyOrExit(!isPartitionIdSame);
 
-            VerifyOrExit(MleRouter::ComparePartitions(connectivity.GetActiveRouters() <= 1, leaderData,
+            VerifyOrExit(MleRouter::ComparePartitions(connectivityTlv.GetActiveRouters() <= 1, leaderData,
                                                       Get<MleRouter>().IsSingleton(), mLeaderData) > 0);
             break;
         }
@@ -3195,8 +3249,8 @@
 #if OPENTHREAD_FTD
         if (IsFullThreadDevice())
         {
-            compare = MleRouter::ComparePartitions(connectivity.GetActiveRouters() <= 1, leaderData, mParentIsSingleton,
-                                                   mParentLeaderData);
+            compare = MleRouter::ComparePartitions(connectivityTlv.GetActiveRouters() <= 1, leaderData,
+                                                   mParentCandidate.mIsSingleton, mParentCandidate.mLeaderData);
         }
 
         // only consider partitions that are the same or better
@@ -3204,14 +3258,10 @@
 #endif
 
         // only consider better parents if the partitions are the same
-#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-        VerifyOrExit(compare != 0 ||
-                     IsBetterParent(sourceAddress, linkQuality, linkMargin, connectivity, static_cast<uint8_t>(version),
-                                    clockAccuracy.GetCslClockAccuracy(), clockAccuracy.GetCslUncertainty()));
-#else
-        VerifyOrExit(compare != 0 || IsBetterParent(sourceAddress, linkQuality, linkMargin, connectivity,
-                                                    static_cast<uint8_t>(version), 0, 0));
-#endif
+        if (compare == 0)
+        {
+            VerifyOrExit(IsBetterParent(sourceAddress, linkQuality, linkMargin, connectivityTlv, version, cslAccuracy));
+        }
     }
 
     // Link/MLE Frame Counters
@@ -3220,12 +3270,12 @@
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 
     // Time Parameter
-    if (Tlv::FindTlv(aRxInfo.mMessage, timeParameter) == kErrorNone)
+    if (Tlv::FindTlv(aRxInfo.mMessage, timeParameterTlv) == kErrorNone)
     {
-        VerifyOrExit(timeParameter.IsValid());
+        VerifyOrExit(timeParameterTlv.IsValid());
 
-        Get<TimeSync>().SetTimeSyncPeriod(timeParameter.GetTimeSyncPeriod());
-        Get<TimeSync>().SetXtalThreshold(timeParameter.GetXtalThreshold());
+        Get<TimeSync>().SetTimeSyncPeriod(timeParameterTlv.GetTimeSyncPeriod());
+        Get<TimeSync>().SetXtalThreshold(timeParameterTlv.GetXtalThreshold());
     }
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_REQUIRED
@@ -3239,37 +3289,33 @@
 #endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 
     // Challenge
-    SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(mParentCandidateChallenge));
+    SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(mParentCandidate.mChallenge));
 
-    mParentCandidate.SetExtAddress(extAddress);
+    InitNeighbor(mParentCandidate, aRxInfo);
     mParentCandidate.SetRloc16(sourceAddress);
     mParentCandidate.GetLinkFrameCounters().SetAll(linkFrameCounter);
     mParentCandidate.SetLinkAckFrameCounter(linkFrameCounter);
     mParentCandidate.SetMleFrameCounter(mleFrameCounter);
-    mParentCandidate.SetVersion(static_cast<uint8_t>(version));
+    mParentCandidate.SetVersion(version);
     mParentCandidate.SetDeviceMode(DeviceMode(DeviceMode::kModeFullThreadDevice | DeviceMode::kModeRxOnWhenIdle |
                                               DeviceMode::kModeFullNetworkData));
-    mParentCandidate.GetLinkInfo().Clear();
-    mParentCandidate.GetLinkInfo().AddRss(linkInfo->GetRss());
-    mParentCandidate.ResetLinkFailures();
-    mParentCandidate.SetLinkQualityOut(LinkQualityInfo::ConvertLinkMarginToLinkQuality(linkMarginFromTlv));
+    mParentCandidate.SetLinkQualityOut(LinkQualityForLinkMargin(linkMarginFromTlv));
     mParentCandidate.SetState(Neighbor::kStateParentResponse);
     mParentCandidate.SetKeySequence(aRxInfo.mKeySequence);
+    mParentCandidate.SetLeaderCost(connectivityTlv.GetLeaderCost());
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    mParentCandidate.SetCslClockAccuracy(clockAccuracy.GetCslClockAccuracy());
-    mParentCandidate.SetCslUncertainty(clockAccuracy.GetCslUncertainty());
+    mParentCandidate.SetCslAccuracy(cslAccuracy);
 #endif
 
-    mParentPriority         = connectivity.GetParentPriority();
-    mParentLinkQuality3     = connectivity.GetLinkQuality3();
-    mParentLinkQuality2     = connectivity.GetLinkQuality2();
-    mParentLinkQuality1     = connectivity.GetLinkQuality1();
-    mParentLeaderCost       = connectivity.GetLeaderCost();
-    mParentSedBufferSize    = connectivity.GetSedBufferSize();
-    mParentSedDatagramCount = connectivity.GetSedDatagramCount();
-    mParentLeaderData       = leaderData;
-    mParentIsSingleton      = connectivity.GetActiveRouters() <= 1;
-    mParentLinkMargin       = linkMargin;
+    mParentCandidate.mPriority         = connectivityTlv.GetParentPriority();
+    mParentCandidate.mLinkQuality3     = connectivityTlv.GetLinkQuality3();
+    mParentCandidate.mLinkQuality2     = connectivityTlv.GetLinkQuality2();
+    mParentCandidate.mLinkQuality1     = connectivityTlv.GetLinkQuality1();
+    mParentCandidate.mSedBufferSize    = connectivityTlv.GetSedBufferSize();
+    mParentCandidate.mSedDatagramCount = connectivityTlv.GetSedDatagramCount();
+    mParentCandidate.mLeaderData       = leaderData;
+    mParentCandidate.mIsSingleton      = connectivityTlv.GetActiveRouters() <= 1;
+    mParentCandidate.mLinkMargin       = linkMargin;
 
 exit:
     LogProcessError(kTypeParentResponse, error);
@@ -3282,16 +3328,17 @@
     uint16_t           sourceAddress;
     uint16_t           shortAddress;
     MeshCoP::Timestamp timestamp;
-    Tlv                tlv;
     uint16_t           networkDataOffset;
+    uint16_t           networkDataLength;
     uint16_t           offset;
+    uint16_t           length;
 
     // Source Address
     SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));
 
     Log(kMessageReceive, kTypeChildIdResponse, aRxInfo.mMessageInfo.GetPeerAddr(), sourceAddress);
 
-    VerifyOrExit(aRxInfo.mNeighbor && aRxInfo.mNeighbor->IsStateValid(), error = kErrorSecurity);
+    VerifyOrExit(aRxInfo.IsNeighborStateValid(), error = kErrorSecurity);
 
     VerifyOrExit(mAttachState == kAttachStateChildIdRequest);
 
@@ -3303,18 +3350,18 @@
     SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
 
     // Network Data
-    SuccessOrExit(error = Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kNetworkData, networkDataOffset));
+    SuccessOrExit(
+        error = Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kNetworkData, networkDataOffset, networkDataLength));
 
     // Active Timestamp
     switch (Tlv::Find<ActiveTimestampTlv>(aRxInfo.mMessage, timestamp))
     {
     case kErrorNone:
         // Active Dataset
-        if (Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kActiveDataset, offset) == kErrorNone)
+        if (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kActiveDataset, offset, length) == kErrorNone)
         {
-            IgnoreError(aRxInfo.mMessage.Read(offset, tlv));
-            SuccessOrExit(error = Get<MeshCoP::ActiveDatasetManager>().Save(timestamp, aRxInfo.mMessage,
-                                                                            offset + sizeof(tlv), tlv.GetLength()));
+            SuccessOrExit(error =
+                              Get<MeshCoP::ActiveDatasetManager>().Save(timestamp, aRxInfo.mMessage, offset, length));
         }
         break;
 
@@ -3336,11 +3383,9 @@
     {
     case kErrorNone:
         // Pending Dataset
-        if (Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kPendingDataset, offset) == kErrorNone)
+        if (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kPendingDataset, offset, length) == kErrorNone)
         {
-            IgnoreError(aRxInfo.mMessage.Read(offset, tlv));
-            IgnoreError(Get<MeshCoP::PendingDatasetManager>().Save(timestamp, aRxInfo.mMessage, offset + sizeof(tlv),
-                                                                   tlv.GetLength()));
+            IgnoreError(Get<MeshCoP::PendingDatasetManager>().Save(timestamp, aRxInfo.mMessage, offset, length));
         }
         break;
 
@@ -3367,32 +3412,21 @@
     SetLeaderData(leaderData.GetPartitionId(), leaderData.GetWeighting(), leaderData.GetLeaderRouterId());
 
 #if OPENTHREAD_FTD
-    if (IsFullThreadDevice())
-    {
-        switch (Get<MleRouter>().ProcessRouteTlv(aRxInfo))
-        {
-        case kErrorNone:
-        case kErrorNotFound:
-            break;
-        default:
-            ExitNow(error = kErrorParse);
-        }
-    }
+    SuccessOrExit(error = Get<MleRouter>().ReadAndProcessRouteTlvOnFed(aRxInfo, RouterIdFromRloc16(sourceAddress)));
 #endif
 
-    mParent = mParentCandidate;
+    mParentCandidate.CopyTo(mParent);
     mParentCandidate.Clear();
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    Get<Mac::Mac>().SetCslParentUncertainty(mParent.GetCslUncertainty());
-    Get<Mac::Mac>().SetCslParentClockAccuracy(mParent.GetCslClockAccuracy());
+    Get<Mac::Mac>().SetCslParentAccuracy(mParent.GetCslAccuracy());
 #endif
 
     mParent.SetRloc16(sourceAddress);
 
-    IgnoreError(Get<NetworkData::Leader>().SetNetworkData(leaderData.GetDataVersion(NetworkData::kFullSet),
-                                                          leaderData.GetDataVersion(NetworkData::kStableSubset),
-                                                          GetNetworkDataType(), aRxInfo.mMessage, networkDataOffset));
+    IgnoreError(Get<NetworkData::Leader>().SetNetworkData(
+        leaderData.GetDataVersion(NetworkData::kFullSet), leaderData.GetDataVersion(NetworkData::kStableSubset),
+        GetNetworkDataType(), aRxInfo.mMessage, networkDataOffset, networkDataLength));
 
     SetStateChild(shortAddress);
 
@@ -3414,14 +3448,11 @@
 
 void Mle::HandleChildUpdateRequest(RxInfo &aRxInfo)
 {
-    static const uint8_t kMaxResponseTlvs = 6;
-
-    Error         error = kErrorNone;
-    uint16_t      sourceAddress;
-    Challenge     challenge;
-    RequestedTlvs requestedTlvs;
-    uint8_t       tlvs[kMaxResponseTlvs] = {};
-    uint8_t       numTlvs                = 0;
+    Error     error = kErrorNone;
+    uint16_t  sourceAddress;
+    Challenge challenge;
+    TlvList   requestedTlvList;
+    TlvList   tlvList;
 
     // Source Address
     SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));
@@ -3432,9 +3463,9 @@
     switch (aRxInfo.mMessage.ReadChallengeTlv(challenge))
     {
     case kErrorNone:
-        tlvs[numTlvs++] = Tlv::kResponse;
-        tlvs[numTlvs++] = Tlv::kMleFrameCounter;
-        tlvs[numTlvs++] = Tlv::kLinkFrameCounter;
+        tlvList.Add(Tlv::kResponse);
+        tlvList.Add(Tlv::kMleFrameCounter);
+        tlvList.Add(Tlv::kLinkFrameCounter);
         break;
     case kErrorNotFound:
         challenge.mLength = 0;
@@ -3468,34 +3499,28 @@
         SuccessOrExit(error = HandleLeaderData(aRxInfo));
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-        CslClockAccuracyTlv cslClockAccuracyTlv;
-        if (Tlv::FindTlv(aRxInfo.mMessage, cslClockAccuracyTlv) == kErrorNone)
         {
-            // MUST include CSL timeout TLV when request includes CSL accuracy
-            tlvs[numTlvs++] = Tlv::kCslTimeout;
+            Mac::CslAccuracy cslAccuracy;
+
+            if (aRxInfo.mMessage.ReadCslClockAccuracyTlv(cslAccuracy) == kErrorNone)
+            {
+                // MUST include CSL timeout TLV when request includes CSL accuracy
+                tlvList.Add(Tlv::kCslTimeout);
+            }
         }
 #endif
     }
     else
     {
         // this device is not a child of the Child Update Request source
-        tlvs[numTlvs++] = Tlv::kStatus;
+        tlvList.Add(Tlv::kStatus);
     }
 
     // TLV Request
-    switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvs))
+    switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
     {
     case kErrorNone:
-        for (uint8_t i = 0; i < requestedTlvs.mNumTlvs; i++)
-        {
-            if (numTlvs >= sizeof(tlvs))
-            {
-                LogWarn("Failed to respond with TLVs: %d of %d", i, requestedTlvs.mNumTlvs);
-                break;
-            }
-
-            tlvs[numTlvs++] = requestedTlvs.mTlvs[i];
-        }
+        tlvList.AddElementsFrom(requestedTlvList);
         break;
     case kErrorNotFound:
         break;
@@ -3512,7 +3537,7 @@
     }
 #endif
 
-    SuccessOrExit(error = SendChildUpdateResponse(tlvs, numTlvs, challenge));
+    SuccessOrExit(error = SendChildUpdateResponse(tlvList, challenge));
 
 exit:
     LogProcessError(kTypeChildUpdateRequestOfParent, error);
@@ -3528,9 +3553,6 @@
     uint32_t  mleFrameCounter;
     uint16_t  sourceAddress;
     uint32_t  timeout;
-#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    CslClockAccuracyTlv clockAccuracy;
-#endif
 
     Log(kMessageReceive, kTypeChildUpdateResponseOfParent, aRxInfo.mMessageInfo.GetPeerAddr());
 
@@ -3557,7 +3579,6 @@
 
     default:
         OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
     }
 
     // Status
@@ -3585,6 +3606,13 @@
 
         mRetrieveNewNetworkData = true;
 
+#if OPENTHREAD_FTD
+        if (IsFullThreadDevice())
+        {
+            mRequestRouteTlv = true;
+        }
+#endif
+
         OT_FALL_THROUGH;
 
     case kRoleChild:
@@ -3620,11 +3648,20 @@
         }
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-        // CSL Accuracy
-        if (Tlv::FindTlv(aRxInfo.mMessage, clockAccuracy) != kErrorNone)
         {
-            Get<Mac::Mac>().SetCslParentClockAccuracy(clockAccuracy.GetCslClockAccuracy());
-            Get<Mac::Mac>().SetCslParentUncertainty(clockAccuracy.GetCslUncertainty());
+            Mac::CslAccuracy cslAccuracy;
+
+            // CSL Accuracy
+            switch (aRxInfo.mMessage.ReadCslClockAccuracyTlv(cslAccuracy))
+            {
+            case kErrorNone:
+                Get<Mac::Mac>().SetCslParentAccuracy(cslAccuracy);
+                break;
+            case kErrorNotFound:
+                break;
+            default:
+                ExitNow(error = kErrorParse);
+            }
         }
 #endif
 
@@ -3642,7 +3679,6 @@
 
     default:
         OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
     }
 
     aRxInfo.mClass = (response.mLength == 0) ? RxInfo::kPeerMessage : RxInfo::kAuthoritativeMessage;
@@ -3743,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);
 
@@ -3765,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;
 
@@ -3782,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());
 
@@ -3821,6 +3857,24 @@
     return (mParent.IsStateValid()) ? mParent.GetRloc16() : static_cast<uint16_t>(Mac::kShortAddrInvalid);
 }
 
+Error Mle::GetParentInfo(Router::Info &aParentInfo) const
+{
+    Error error = kErrorNone;
+
+    // Skip the check for reference device since it needs to get the
+    // original parent's info even after role change.
+
+#if !OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+    VerifyOrExit(IsChild(), error = kErrorInvalidState);
+#endif
+
+    aParentInfo.SetFrom(mParent);
+    ExitNow();
+
+exit:
+    return error;
+}
+
 bool Mle::IsRoutingLocator(const Ip6::Address &aAddress) const
 {
     return IsMeshLocalAddress(aAddress) && aAddress.GetIid().IsRoutingLocator();
@@ -3856,7 +3910,7 @@
 void Mle::InformPreviousParent(void)
 {
     Error            error   = kErrorNone;
-    Message *        message = nullptr;
+    Message         *message = nullptr;
     Ip6::MessageInfo messageInfo;
 
     VerifyOrExit((message = Get<Ip6::Ip6>().NewMessage(0)) != nullptr, error = kErrorNoBufs);
@@ -3882,70 +3936,65 @@
 #endif // OPENTHREAD_CONFIG_MLE_INFORM_PREVIOUS_PARENT_ON_REATTACH
 
 #if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
-void Mle::HandleParentSearchTimer(Timer &aTimer)
-{
-    aTimer.Get<Mle>().HandleParentSearchTimer();
-}
-
-void Mle::HandleParentSearchTimer(void)
+void Mle::ParentSearch::HandleTimer(void)
 {
     int8_t parentRss;
 
-    LogInfo("PeriodicParentSearch: %s interval passed", mParentSearchIsInBackoff ? "Backoff" : "Check");
+    LogInfo("PeriodicParentSearch: %s interval passed", mIsInBackoff ? "Backoff" : "Check");
 
-    if (mParentSearchBackoffWasCanceled)
+    if (mBackoffWasCanceled)
     {
         // Backoff can be canceled if the device switches to a new parent.
         // from `UpdateParentSearchState()`. We want to limit this to happen
         // only once within a backoff interval.
 
-        if (TimerMilli::GetNow() - mParentSearchBackoffCancelTime >= kParentSearchBackoffInterval)
+        if (TimerMilli::GetNow() - mBackoffCancelTime >= kBackoffInterval)
         {
-            mParentSearchBackoffWasCanceled = false;
+            mBackoffWasCanceled = false;
             LogInfo("PeriodicParentSearch: Backoff cancellation is allowed on parent switch");
         }
     }
 
-    mParentSearchIsInBackoff = false;
+    mIsInBackoff = false;
 
-    VerifyOrExit(IsChild());
+    VerifyOrExit(Get<Mle>().IsChild());
 
-    parentRss = GetParent().GetLinkInfo().GetAverageRss();
+    parentRss = Get<Mle>().GetParent().GetLinkInfo().GetAverageRss();
     LogInfo("PeriodicParentSearch: Parent RSS %d", parentRss);
-    VerifyOrExit(parentRss != OT_RADIO_RSSI_INVALID);
+    VerifyOrExit(parentRss != Radio::kInvalidRssi);
 
-    if (parentRss < kParentSearchRssThreadhold)
+    if (parentRss < kRssThreshold)
     {
-        LogInfo("PeriodicParentSearch: Parent RSS less than %d, searching for new parents", kParentSearchRssThreadhold);
-        mParentSearchIsInBackoff = true;
-        Attach(kBetterParent);
+        LogInfo("PeriodicParentSearch: Parent RSS less than %d, searching for new parents", kRssThreshold);
+        mIsInBackoff = true;
+        Get<Mle>().Attach(kBetterParent);
     }
 
 exit:
-    StartParentSearchTimer();
+    StartTimer();
 }
 
-void Mle::StartParentSearchTimer(void)
+void Mle::ParentSearch::StartTimer(void)
 {
     uint32_t interval;
 
-    interval = Random::NonCrypto::GetUint32InRange(0, kParentSearchJitterInterval);
+    interval = Random::NonCrypto::GetUint32InRange(0, kJitterInterval);
 
-    if (mParentSearchIsInBackoff)
+    if (mIsInBackoff)
     {
-        interval += kParentSearchBackoffInterval;
+        interval += kBackoffInterval;
     }
     else
     {
-        interval += kParentSearchCheckInterval;
+        interval += kCheckInterval;
     }
 
-    mParentSearchTimer.Start(interval);
+    mTimer.Start(interval);
 
-    LogInfo("PeriodicParentSearch: (Re)starting timer for %s interval", mParentSearchIsInBackoff ? "backoff" : "check");
+    LogInfo("PeriodicParentSearch: (Re)starting timer for %s interval", mIsInBackoff ? "backoff" : "check");
 }
 
-void Mle::UpdateParentSearchState(void)
+void Mle::ParentSearch::UpdateState(void)
 {
 #if OPENTHREAD_CONFIG_MLE_INFORM_PREVIOUS_PARENT_ON_REATTACH
 
@@ -3964,24 +4013,25 @@
     // the chance to switch back to the original (and possibly
     // preferred) parent more quickly.
 
-    if (mParentSearchIsInBackoff && !mParentSearchBackoffWasCanceled && mParentSearchRecentlyDetached)
+    if (mIsInBackoff && !mBackoffWasCanceled && mRecentlyDetached)
     {
-        if ((mPreviousParentRloc != Mac::kShortAddrInvalid) && (mPreviousParentRloc != mParent.GetRloc16()))
+        if ((Get<Mle>().mPreviousParentRloc != Mac::kShortAddrInvalid) &&
+            (Get<Mle>().mPreviousParentRloc != Get<Mle>().mParent.GetRloc16()))
         {
-            mParentSearchIsInBackoff        = false;
-            mParentSearchBackoffWasCanceled = true;
-            mParentSearchBackoffCancelTime  = TimerMilli::GetNow();
+            mIsInBackoff        = false;
+            mBackoffWasCanceled = true;
+            mBackoffCancelTime  = TimerMilli::GetNow();
             LogInfo("PeriodicParentSearch: Canceling backoff on switching to a new parent");
         }
     }
 
 #endif // OPENTHREAD_CONFIG_MLE_INFORM_PREVIOUS_PARENT_ON_REATTACH
 
-    mParentSearchRecentlyDetached = false;
+    mRecentlyDetached = false;
 
-    if (!mParentSearchIsInBackoff)
+    if (!mIsInBackoff)
     {
-        StartParentSearchTimer();
+        StartTimer();
     }
 }
 #endif // OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
@@ -4012,15 +4062,9 @@
 #endif
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
-void Mle::LogProcessError(MessageType aType, Error aError)
-{
-    LogError(kMessageReceive, aType, aError);
-}
+void Mle::LogProcessError(MessageType aType, Error aError) { LogError(kMessageReceive, aType, aError); }
 
-void Mle::LogSendError(MessageType aType, Error aError)
-{
-    LogError(kMessageSend, aType, aError);
-}
+void Mle::LogSendError(MessageType aType, Error aError) { LogError(kMessageSend, aType, aError); }
 
 void Mle::LogError(MessageAction aAction, MessageType aType, Error aError)
 {
@@ -4190,25 +4234,6 @@
 
 #endif // #if OT_SHOULD_LOG_AT( OT_LOG_LEVEL_WARN)
 
-const char *Mle::RoleToString(DeviceRole aRole)
-{
-    static const char *const kRoleStrings[] = {
-        "disabled", // (0) kRoleDisabled
-        "detached", // (1) kRoleDetached
-        "child",    // (2) kRoleChild
-        "router",   // (3) kRoleRouter
-        "leader",   // (4) kRoleLeader
-    };
-
-    static_assert(kRoleDisabled == 0, "kRoleDisabled value is incorrect");
-    static_assert(kRoleDetached == 1, "kRoleDetached value is incorrect");
-    static_assert(kRoleChild == 2, "kRoleChild value is incorrect");
-    static_assert(kRoleRouter == 3, "kRoleRouter value is incorrect");
-    static_assert(kRoleLeader == 4, "kRoleLeader value is incorrect");
-
-    return (aRole < GetArrayLength(kRoleStrings)) ? kRoleStrings[aRole] : "invalid";
-}
-
 // LCOV_EXCL_START
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_NOTE)
@@ -4277,22 +4302,21 @@
 // LCOV_EXCL_STOP
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
-Error Mle::SendLinkMetricsManagementRequest(const Ip6::Address &aDestination, const uint8_t *aSubTlvs, uint8_t aLength)
+Error Mle::SendLinkMetricsManagementRequest(const Ip6::Address &aDestination, const ot::Tlv &aSubTlv)
 {
-    Error      error = kErrorNone;
-    TxMessage *message;
+    Error      error   = kErrorNone;
+    TxMessage *message = NewMleMessage(kCommandLinkMetricsManagementRequest);
     Tlv        tlv;
 
-    VerifyOrExit((message = NewMleMessage(kCommandLinkMetricsManagementRequest)) != nullptr, error = kErrorNoBufs);
+    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
-    // Link Metrics Management TLV
     tlv.SetType(Tlv::kLinkMetricsManagement);
-    tlv.SetLength(aLength);
+    tlv.SetLength(static_cast<uint8_t>(aSubTlv.GetSize()));
 
-    SuccessOrExit(error = message->AppendBytes(&tlv, sizeof(tlv)));
-    SuccessOrExit(error = message->AppendBytes(aSubTlvs, aLength));
+    SuccessOrExit(error = message->Append(tlv));
+    SuccessOrExit(error = aSubTlv.AppendTo(*message));
 
-    SuccessOrExit(error = message->SendTo(aDestination));
+    error = message->SendTo(aDestination);
 
 exit:
     FreeMessageOnError(message, error);
@@ -4300,96 +4324,94 @@
 }
 #endif
 
-void Mle::RegisterParentResponseStatsCallback(otThreadParentResponseCallback aCallback, void *aContext)
-{
-    mParentResponseCb        = aCallback;
-    mParentResponseCbContext = aContext;
-}
-
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-uint64_t Mle::CalcParentCslMetric(uint8_t aCslClockAccuracy, uint8_t aCslUncertainty)
+uint64_t Mle::CalcParentCslMetric(const Mac::CslAccuracy &aCslAccuracy) const
 {
-    /*
-     * This function calculates the overall time that device will operate on battery
-     * by summming sequence of "ON quants" over a period of time.
-     */
-    const uint64_t usInSecond   = 1000000;
-    uint64_t       cslPeriodUs  = kMinCslPeriod * kUsPerTenSymbols;
-    uint64_t       cslTimeoutUs = GetCslTimeout() * usInSecond;
-    uint64_t       k            = cslTimeoutUs / cslPeriodUs;
+    // This function calculates the overall time that device will operate
+    // on battery by summing sequence of "ON quants" over a period of time.
 
-    return k * (k + 1) * cslPeriodUs / usInSecond * aCslClockAccuracy + aCslUncertainty * k * kUsPerUncertUnit;
+    static constexpr uint64_t usInSecond = 1000000;
+
+    uint64_t cslPeriodUs  = kMinCslPeriod * kUsPerTenSymbols;
+    uint64_t cslTimeoutUs = GetCslTimeout() * usInSecond;
+    uint64_t k            = cslTimeoutUs / cslPeriodUs;
+
+    return k * (k + 1) * cslPeriodUs / usInSecond * aCslAccuracy.GetClockAccuracy() +
+           aCslAccuracy.GetUncertaintyInMicrosec() * k;
 }
 #endif
 
 Error Mle::DetachGracefully(otDetachGracefullyCallback aCallback, void *aContext)
 {
-    Error error = kErrorNone;
+    Error    error   = kErrorNone;
+    uint32_t timeout = kDetachGracefullyTimeout;
 
     VerifyOrExit(!IsDetachingGracefully(), error = kErrorBusy);
 
-    OT_ASSERT(mDetachGracefullyCallback == nullptr);
+    OT_ASSERT(!mDetachGracefullyCallback.IsSet());
 
-    mDetachGracefullyCallback = aCallback;
-    mDetachGracefullyContext  = aContext;
+    mDetachGracefullyCallback.Set(aCallback, aContext);
 
-    if (IsChild() || IsRouter())
-    {
-        mDetachGracefullyTimer.Start(kDetachGracefullyTimeout);
-    }
-    else
-    {
-        // If the device is a leader, or it's already detached or disabled, we start the timer with zero duration to
-        // stop and invoke the callback when the timer fires, so the operation finishes immediately and asynchronously.
-        mDetachGracefullyTimer.Start(0);
-    }
-
-    if (IsChild())
-    {
-        IgnoreError(SendChildUpdateRequest(/* aAppendChallenge */ false, /* aTimeout */ 0));
-    }
-#if OPENTHREAD_FTD
-    else if (IsRouter())
-    {
-        Get<MleRouter>().SendAddressRelease(&Mle::HandleDetachGracefullyAddressReleaseResponse, this);
-    }
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    Get<BorderRouter::RoutingManager>().RequestStop();
 #endif
 
+    switch (mRole)
+    {
+    case kRoleLeader:
+        break;
+
+    case kRoleRouter:
+#if OPENTHREAD_FTD
+        Get<MleRouter>().SendAddressRelease();
+#endif
+        break;
+
+    case kRoleChild:
+        IgnoreError(SendChildUpdateRequest(kAppendZeroTimeout));
+        break;
+
+    case kRoleDisabled:
+    case kRoleDetached:
+        // If device is already detached or disabled, we start the timer
+        // with zero duration to stop and invoke the callback when the
+        // timer fires, so the operation finishes immediately and
+        // asynchronously.
+        timeout = 0;
+        break;
+    }
+
+    mDetachGracefullyTimer.Start(timeout);
+
 exit:
     return error;
 }
 
-void Mle::HandleDetachGracefullyTimer(Timer &aTimer)
-{
-    aTimer.Get<Mle>().HandleDetachGracefullyTimer();
-}
+void Mle::HandleDetachGracefullyTimer(void) { Stop(); }
 
-void Mle::HandleDetachGracefullyTimer(void)
-{
-    Stop();
-}
+//---------------------------------------------------------------------------------------------------------------------
+// TlvList
 
-#if OPENTHREAD_FTD
-void Mle::HandleDetachGracefullyAddressReleaseResponse(void *               aContext,
-                                                       otMessage *          aMessage,
-                                                       const otMessageInfo *aMessageInfo,
-                                                       Error                aResult)
+void Mle::TlvList::Add(uint8_t aTlvType)
 {
-    OT_UNUSED_VARIABLE(aMessage);
-    OT_UNUSED_VARIABLE(aMessageInfo);
-    OT_UNUSED_VARIABLE(aResult);
+    VerifyOrExit(!Contains(aTlvType));
 
-    static_cast<MleRouter *>(aContext)->HandleDetachGracefullyAddressReleaseResponse();
-}
-
-void Mle::HandleDetachGracefullyAddressReleaseResponse(void)
-{
-    if (IsDetachingGracefully())
+    if (PushBack(aTlvType) != kErrorNone)
     {
-        Stop();
+        LogWarn("Failed to include TLV %d", aTlvType);
+    }
+
+exit:
+    return;
+}
+
+void Mle::TlvList::AddElementsFrom(const TlvList &aTlvList)
+{
+    for (uint8_t tlvType : aTlvList)
+    {
+        Add(tlvType);
     }
 }
-#endif // OPENTHREAD_FTD
 
 //---------------------------------------------------------------------------------------------------------------------
 // Challenge
@@ -4427,7 +4449,7 @@
 Mle::TxMessage *Mle::NewMleMessage(Command aCommand)
 {
     Error             error = kErrorNone;
-    TxMessage *       message;
+    TxMessage        *message;
     Message::Settings settings(Message::kNoLinkSecurity, Message::kPriorityNet);
     Message::SubType  subType;
     uint8_t           securitySuite;
@@ -4501,20 +4523,11 @@
     return Tlv::Append<SourceAddressTlv>(*this, Get<Mle>().GetRloc16());
 }
 
-Error Mle::TxMessage::AppendStatusTlv(StatusTlv::Status aStatus)
-{
-    return Tlv::Append<StatusTlv>(*this, aStatus);
-}
+Error Mle::TxMessage::AppendStatusTlv(StatusTlv::Status aStatus) { return Tlv::Append<StatusTlv>(*this, aStatus); }
 
-Error Mle::TxMessage::AppendModeTlv(DeviceMode aMode)
-{
-    return Tlv::Append<ModeTlv>(*this, aMode.Get());
-}
+Error Mle::TxMessage::AppendModeTlv(DeviceMode aMode) { return Tlv::Append<ModeTlv>(*this, aMode.Get()); }
 
-Error Mle::TxMessage::AppendTimeoutTlv(uint32_t aTimeout)
-{
-    return Tlv::Append<TimeoutTlv>(*this, aTimeout);
-}
+Error Mle::TxMessage::AppendTimeoutTlv(uint32_t aTimeout) { return Tlv::Append<TimeoutTlv>(*this, aTimeout); }
 
 Error Mle::TxMessage::AppendChallengeTlv(const Challenge &aChallenge)
 {
@@ -4543,7 +4556,7 @@
     counter = Get<KeyManager>().GetMaximumMacFrameCounter();
 
 #if OPENTHREAD_CONFIG_MULTI_RADIO
-    Get<KeyManager>().SetAllMacFrameCounters(counter);
+    Get<KeyManager>().SetAllMacFrameCounters(counter, /* aSetIfLarger */ true);
 #endif
 
     return Tlv::Append<LinkFrameCounterTlv>(*this, counter);
@@ -4554,10 +4567,7 @@
     return Tlv::Append<MleFrameCounterTlv>(*this, Get<KeyManager>().GetMleFrameCounter());
 }
 
-Error Mle::TxMessage::AppendAddress16Tlv(uint16_t aRloc16)
-{
-    return Tlv::Append<Address16Tlv>(*this, aRloc16);
-}
+Error Mle::TxMessage::AppendAddress16Tlv(uint16_t aRloc16) { return Tlv::Append<Address16Tlv>(*this, aRloc16); }
 
 Error Mle::TxMessage::AppendLeaderDataTlv(void)
 {
@@ -4594,63 +4604,43 @@
     return Tlv::Append<TlvRequestTlv>(*this, aTlvs, aTlvsLength);
 }
 
-Error Mle::TxMessage::AppendScanMaskTlv(uint8_t aScanMask)
-{
-    return Tlv::Append<ScanMaskTlv>(*this, aScanMask);
-}
+Error Mle::TxMessage::AppendScanMaskTlv(uint8_t aScanMask) { return Tlv::Append<ScanMaskTlv>(*this, aScanMask); }
 
 Error Mle::TxMessage::AppendLinkMarginTlv(uint8_t aLinkMargin)
 {
     return Tlv::Append<LinkMarginTlv>(*this, aLinkMargin);
 }
 
-Error Mle::TxMessage::AppendVersionTlv(void)
-{
-    return Tlv::Append<VersionTlv>(*this, kThreadVersion);
-}
+Error Mle::TxMessage::AppendVersionTlv(void) { return Tlv::Append<VersionTlv>(*this, kThreadVersion); }
 
 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();
-#if OPENTHREAD_CONFIG_DUA_ENABLE
-    Ip6::Address domainUnicastAddress;
-#endif
+    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);
     counter++;
 
 #if OPENTHREAD_CONFIG_DUA_ENABLE
-    // Cache Domain Unicast Address.
-    domainUnicastAddress = Get<DuaManager>().GetDomainUnicastAddress();
-
-    if (Get<ThreadNetif>().HasUnicastAddress(domainUnicastAddress))
+    if (Get<ThreadNetif>().HasUnicastAddress(Get<DuaManager>().GetDomainUnicastAddress()) &&
+        (Get<NetworkData::Leader>().GetContext(Get<DuaManager>().GetDomainUnicastAddress(), context) == kErrorNone))
     {
-        SuccessOrAssert(Get<NetworkData::Leader>().GetContext(domainUnicastAddress, context));
-
         // Prioritize DUA, compressed entry
-        entry.SetContextId(context.mContextId);
-        entry.SetIid(domainUnicastAddress.GetIid());
-        SuccessOrExit(error = AppendBytes(&entry, entry.GetLength()));
-        length += entry.GetLength();
+        SuccessOrExit(
+            error = AppendCompressedAddressEntry(context.mContextId, Get<DuaManager>().GetDomainUnicastAddress()));
         counter++;
     }
-#endif // OPENTHREAD_CONFIG_DUA_ENABLE
+#endif
 
     for (const Ip6::Netif::UnicastAddress &addr : Get<ThreadNetif>().GetUnicastAddresses())
     {
@@ -4661,8 +4651,7 @@
         }
 
 #if OPENTHREAD_CONFIG_DUA_ENABLE
-        // Skip DUA that was already appended above.
-        if (addr.GetAddress() == domainUnicastAddress)
+        if (addr.GetAddress() == Get<DuaManager>().GetDomainUnicastAddress())
         {
             continue;
         }
@@ -4670,22 +4659,16 @@
 
         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 < OPENTHREAD_CONFIG_MLE_IP_ADDRS_TO_REGISTER);
+        VerifyOrExit(counter < kMaxIpAddressesToRegister);
     }
 
     // Append external multicast addresses.  For sleepy end device,
@@ -4710,28 +4693,58 @@
             }
 #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 < OPENTHREAD_CONFIG_MLE_IP_ADDRS_TO_REGISTER);
+            VerifyOrExit(counter < kMaxIpAddressesToRegister);
         }
     }
 
 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);
+}
+
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 Error Mle::TxMessage::AppendTimeRequestTlv(void)
 {
@@ -4783,48 +4796,41 @@
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 Error Mle::TxMessage::AppendCslChannelTlv(void)
 {
-    Error         error = kErrorNone;
     CslChannelTlv cslChannel;
 
-    // In current implementation, it's allowed to set CSL Channel unspecified. As `0` is not valid for Channel value
-    // in CSL Channel TLV, if CSL channel is not specified, we don't append CSL Channel TLV.
-    // And on transmitter side, it would also set CSL Channel for the child to `0` if it doesn't find a CSL Channel
-    // TLV.
-    VerifyOrExit(Get<Mac::Mac>().GetCslChannel());
+    // CSL channel value of zero indicates that the CSL channel is not
+    // specified. We can use this value in the TLV as well.
 
     cslChannel.Init();
     cslChannel.SetChannelPage(0);
     cslChannel.SetChannel(Get<Mac::Mac>().GetCslChannel());
 
-    SuccessOrExit(error = Append(cslChannel));
-
-exit:
-    return error;
+    return Append(cslChannel);
 }
 
 Error Mle::TxMessage::AppendCslTimeoutTlv(void)
 {
-    OT_ASSERT(Get<Mac::Mac>().IsCslEnabled());
-    return Tlv::Append<CslTimeoutTlv>(*this,
-                                      Get<Mle>().mCslTimeout == 0 ? Get<Mle>().mTimeout : Get<Mle>().mCslTimeout);
+    uint32_t timeout = Get<Mle>().GetCslTimeout();
+
+    if (timeout == 0)
+    {
+        timeout = Get<Mle>().GetTimeout();
+    }
+
+    return Tlv::Append<CslTimeoutTlv>(*this, timeout);
 }
 #endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 
 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
 Error Mle::TxMessage::AppendCslClockAccuracyTlv(void)
 {
-    Error               error = kErrorNone;
-    CslClockAccuracyTlv cslClockAccuracy;
+    CslClockAccuracyTlv cslClockAccuracyTlv;
 
-    cslClockAccuracy.Init();
+    cslClockAccuracyTlv.Init();
+    cslClockAccuracyTlv.SetCslClockAccuracy(Get<Radio>().GetCslAccuracy());
+    cslClockAccuracyTlv.SetCslUncertainty(Get<Radio>().GetCslUncertainty());
 
-    cslClockAccuracy.SetCslClockAccuracy(Get<Radio>().GetCslAccuracy());
-    cslClockAccuracy.SetCslUncertainty(Get<Radio>().GetCslUncertainty());
-
-    SuccessOrExit(error = Append(cslClockAccuracy));
-
-exit:
-    return error;
+    return Append(cslClockAccuracyTlv);
 }
 #endif
 
@@ -4896,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));
@@ -4912,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:
@@ -4943,7 +4940,7 @@
     RouteTlv tlv;
 
     tlv.Init();
-    Get<MleRouter>().FillRouteTlv(tlv, aNeighbor);
+    Get<RouterTable>().FillRouteTlv(tlv, aNeighbor);
 
     return tlv.AppendTo(*this);
 }
@@ -4972,10 +4969,7 @@
     SuccessOrExit(error = Tlv::FindTlvValueOffset(*this, aTlvType, offset, length));
     VerifyOrExit(length >= kMinChallengeSize, error = kErrorParse);
 
-    if (length > kMaxChallengeSize)
-    {
-        length = kMaxChallengeSize;
-    }
+    length = Min(length, kMaxChallengeSize);
 
     ReadBytes(offset, aBuffer.mBuffer, length);
     aBuffer.mLength = static_cast<uint8_t>(length);
@@ -5029,7 +5023,7 @@
     return error;
 }
 
-Error Mle::RxMessage::ReadTlvRequestTlv(RequestedTlvs &aRequestedTlvs) const
+Error Mle::RxMessage::ReadTlvRequestTlv(TlvList &aTlvList) const
 {
     Error    error;
     uint16_t offset;
@@ -5037,17 +5031,68 @@
 
     SuccessOrExit(error = Tlv::FindTlvValueOffset(*this, Tlv::kTlvRequest, offset, length));
 
-    if (length > sizeof(aRequestedTlvs.mTlvs))
+    if (length > aTlvList.GetMaxSize())
     {
-        length = sizeof(aRequestedTlvs.mTlvs);
+        length = aTlvList.GetMaxSize();
     }
 
-    ReadBytes(offset, aRequestedTlvs.mTlvs, length);
-    aRequestedTlvs.mNumTlvs = static_cast<uint8_t>(length);
+    ReadBytes(offset, aTlvList.GetArrayBuffer(), length);
+    aTlvList.SetLength(static_cast<uint8_t>(length));
 
 exit:
     return error;
 }
 
+#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+Error Mle::RxMessage::ReadCslClockAccuracyTlv(Mac::CslAccuracy &aCslAccuracy) const
+{
+    Error               error;
+    CslClockAccuracyTlv clockAccuracyTlv;
+
+    SuccessOrExit(error = Tlv::FindTlv(*this, clockAccuracyTlv));
+    VerifyOrExit(clockAccuracyTlv.IsValid(), error = kErrorParse);
+    aCslAccuracy.SetClockAccuracy(clockAccuracyTlv.GetCslClockAccuracy());
+    aCslAccuracy.SetUncertainty(clockAccuracyTlv.GetCslUncertainty());
+
+exit:
+    return error;
+}
+#endif
+
+#if OPENTHREAD_FTD
+Error Mle::RxMessage::ReadRouteTlv(RouteTlv &aRouteTlv) const
+{
+    Error error;
+
+    SuccessOrExit(error = Tlv::FindTlv(*this, aRouteTlv));
+    VerifyOrExit(aRouteTlv.IsValid(), error = kErrorParse);
+
+exit:
+    return error;
+}
+#endif
+
+//---------------------------------------------------------------------------------------------------------------------
+// ParentCandidate
+
+void Mle::ParentCandidate::Clear(void)
+{
+    Instance &instance = GetInstance();
+
+    memset(reinterpret_cast<void *>(this), 0, sizeof(ParentCandidate));
+    Init(instance);
+}
+
+void Mle::ParentCandidate::CopyTo(Parent &aParent) const
+{
+    // We use an intermediate pointer to copy `ParentCandidate`
+    // to silence code checker's warning about object slicing
+    // (assigning a sub-class to base class instance).
+
+    const Parent *candidateAsParent = this;
+
+    aParent = *candidateAsParent;
+}
+
 } // namespace Mle
 } // namespace ot
diff --git a/src/core/thread/mle.hpp b/src/core/thread/mle.hpp
index 5d07436..f33b525 100644
--- a/src/core/thread/mle.hpp
+++ b/src/core/thread/mle.hpp
@@ -36,6 +36,7 @@
 
 #include "openthread-core-config.h"
 
+#include "common/callback.hpp"
 #include "common/encoding.hpp"
 #include "common/locator.hpp"
 #include "common/log.hpp"
@@ -72,6 +73,8 @@
  * @}
  */
 
+class SupervisionListener;
+
 /**
  * @namespace ot::Mle
  *
@@ -99,8 +102,9 @@
 {
     friend class DiscoverScanner;
     friend class ot::Notifier;
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-    friend class ot::LinkMetrics::LinkMetrics;
+    friend class ot::SupervisionListener;
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    friend class ot::LinkMetrics::Initiator;
 #endif
 
 public:
@@ -415,7 +419,26 @@
      * @returns A reference to the parent.
      *
      */
-    Router &GetParent(void) { return mParent; }
+    Parent &GetParent(void) { return mParent; }
+
+    /**
+     * This method gets the parent when operating in End Device mode.
+     *
+     * @returns A reference to the parent.
+     *
+     */
+    const Parent &GetParent(void) const { return mParent; }
+
+    /**
+     * The method retrieves information about the parent.
+     *
+     * @param[out] aParentInfo     Reference to a parent information structure.
+     *
+     * @retval kErrorNone          Successfully retrieved the parent info and updated @p aParentInfo.
+     * @retval kErrorInvalidState  Device role is not child.
+     *
+     */
+    Error GetParentInfo(Router::Info &aParentInfo) const;
 
     /**
      * This method get the parent candidate.
@@ -423,7 +446,17 @@
      * The parent candidate is valid when attempting to attach to a new parent.
      *
      */
-    Router &GetParentCandidate(void) { return mParentCandidate; }
+    Parent &GetParentCandidate(void) { return mParentCandidate; }
+
+    /**
+     * This method starts the process for child to search for a better parent while staying attached to its current
+     * parent
+     *
+     * @retval kErrorNone          Successfully started the process to search for a better parent.
+     * @retval kErrorInvalidState  Device role is not child.
+     *
+     */
+    Error SearchForBetterParent(void);
 
     /**
      * This method indicates whether or not an IPv6 address is an RLOC.
@@ -474,7 +507,7 @@
      * @returns The RLOC16 assigned to the Thread interface.
      *
      */
-    uint16_t GetRloc16(void) const;
+    uint16_t GetRloc16(void) const { return mRloc16; }
 
     /**
      * This method returns a reference to the RLOC assigned to the Thread interface.
@@ -558,100 +591,6 @@
     const LeaderData &GetLeaderData(void);
 
     /**
-     * This method derives the Child ID from a given RLOC16.
-     *
-     * @param[in]  aRloc16  The RLOC16 value.
-     *
-     * @returns The Child ID portion of an RLOC16.
-     *
-     */
-    static uint16_t ChildIdFromRloc16(uint16_t aRloc16) { return aRloc16 & kMaxChildId; }
-
-    /**
-     * This method derives the Router ID portion from a given RLOC16.
-     *
-     * @param[in]  aRloc16  The RLOC16 value.
-     *
-     * @returns The Router ID portion of an RLOC16.
-     *
-     */
-    static uint8_t RouterIdFromRloc16(uint16_t aRloc16) { return aRloc16 >> kRouterIdOffset; }
-
-    /**
-     * This method returns whether the two RLOC16 have the same Router ID.
-     *
-     * @param[in]  aRloc16A  The first RLOC16 value.
-     * @param[in]  aRloc16B  The second RLOC16 value.
-     *
-     * @returns true if the two RLOC16 have the same Router ID, false otherwise.
-     *
-     */
-    static bool RouterIdMatch(uint16_t aRloc16A, uint16_t aRloc16B)
-    {
-        return RouterIdFromRloc16(aRloc16A) == RouterIdFromRloc16(aRloc16B);
-    }
-
-    /**
-     * This method returns the Service ID corresponding to a Service ALOC16.
-     *
-     * @param[in]  aAloc16  The Service ALOC16 value.
-     *
-     * @returns The Service ID corresponding to given ALOC16.
-     *
-     */
-    static uint8_t ServiceIdFromAloc(uint16_t aAloc16) { return static_cast<uint8_t>(aAloc16 - kAloc16ServiceStart); }
-
-    /**
-     * This method returns the Service ALOC16 corresponding to a Service ID.
-     *
-     * @param[in]  aServiceId  The Service ID value.
-     *
-     * @returns The Service ALOC16 corresponding to given ID.
-     *
-     */
-    static uint16_t ServiceAlocFromId(uint8_t aServiceId)
-    {
-        return static_cast<uint16_t>(aServiceId + kAloc16ServiceStart);
-    }
-
-    /**
-     * This method returns the Commissioner Aloc corresponding to a Commissioner Session ID.
-     *
-     * @param[in]  aSessionId   The Commissioner Session ID value.
-     *
-     * @returns The Commissioner ALOC16 corresponding to given ID.
-     *
-     */
-    static uint16_t CommissionerAloc16FromId(uint16_t aSessionId)
-    {
-        return static_cast<uint16_t>((aSessionId & kAloc16CommissionerMask) + kAloc16CommissionerStart);
-    }
-
-    /**
-     * This method derives RLOC16 from a given Router ID.
-     *
-     * @param[in]  aRouterId  The Router ID value.
-     *
-     * @returns The RLOC16 corresponding to the given Router ID.
-     *
-     */
-    static uint16_t Rloc16FromRouterId(uint8_t aRouterId)
-    {
-        return static_cast<uint16_t>(aRouterId << kRouterIdOffset);
-    }
-
-    /**
-     * This method indicates whether or not @p aRloc16 refers to an active router.
-     *
-     * @param[in]  aRloc16  The RLOC16 value.
-     *
-     * @retval TRUE   If @p aRloc16 refers to an active router.
-     * @retval FALSE  If @p aRloc16 does not refer to an active router.
-     *
-     */
-    static bool IsActiveRouter(uint16_t aRloc16) { return ChildIdFromRloc16(aRloc16) == 0; }
-
-    /**
      * This method returns a reference to the send queue.
      *
      * @returns A reference to the send queue.
@@ -666,25 +605,26 @@
     void RemoveDelayedDataResponseMessage(void);
 
     /**
-     * This method converts a device role into a human-readable string.
-     *
-     */
-    static const char *RoleToString(DeviceRole aRole);
-
-    /**
      * This method gets the MLE counters.
      *
      * @returns A reference to the MLE counters.
      *
      */
-    const otMleCounters &GetCounters(void) const { return mCounters; }
+    const Counters &GetCounters(void)
+    {
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+        UpdateRoleTimeCounters(mRole);
+#endif
+        return mCounters;
+    }
 
     /**
      * 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.
      *
@@ -692,7 +632,11 @@
      * @param[in]  aContext  A pointer to application-specific context.
      *
      */
-    void RegisterParentResponseStatsCallback(otThreadParentResponseCallback aCallback, void *aContext);
+    void RegisterParentResponseStatsCallback(otThreadParentResponseCallback aCallback, void *aContext)
+    {
+        mParentResponseCallback.Set(aCallback, aContext);
+    }
+#endif
 
     /**
      * This method requests MLE layer to prepare and send a shorter version of Child ID Request message by only
@@ -752,12 +696,12 @@
     /**
      * This method calculates CSL metric of parent.
      *
-     * @param[in] aCslClockAccuracy The CSL Clock Accuracy.
-     * @param[in] aCslUncertainty The CSL Uncertainty.
+     * @param[in] aCslAccuracy The CSL accuracy.
      *
      * @returns CSL metric.
+     *
      */
-    uint64_t CalcParentCslMetric(uint8_t aCslClockAccuracy, uint8_t aCslUncertainty);
+    uint64_t CalcParentCslMetric(const Mac::CslAccuracy &aCslAccuracy) const;
 
 #endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 
@@ -903,6 +847,40 @@
 #endif
     };
 
+    static constexpr uint8_t kMaxTlvListSize = 32; ///< Maximum number of TLVs in a `TlvList`.
+
+    /**
+     * This type represents a list of TLVs (array of TLV types).
+     *
+     */
+    class TlvList : public Array<uint8_t, kMaxTlvListSize>
+    {
+    public:
+        /**
+         * This constructor initializes the `TlvList` as empty.
+         *
+         */
+        TlvList(void) = default;
+
+        /**
+         * This method checks if a given TLV type is not already present in the list and adds it in the list.
+         *
+         * If the list is full, this method logs it as a warning.
+         *
+         * @param[in] aTlvType   The TLV type to add to the list.
+         *
+         */
+        void Add(uint8_t aTlvType);
+
+        /**
+         * This method adds elements from a given list to this TLV list (if not already present in the list).
+         *
+         * @param[in] aTlvList   The TLV list to add elements from.
+         *
+         */
+        void AddElementsFrom(const TlvList &aTlvList);
+    };
+
     /**
      * This type represents a Challenge (or Response) data.
      *
@@ -943,18 +921,6 @@
     };
 
     /**
-     * This type represents list of requested TLVs in a TLV Request TLV.
-     *
-     */
-    struct RequestedTlvs
-    {
-        static constexpr uint8_t kMaxNumTlvs = 16; ///< Maximum number of TLVs in request array.
-
-        uint8_t mTlvs[kMaxNumTlvs]; ///< Array of requested TLVs.
-        uint8_t mNumTlvs;           ///< Number of TLVs in the array.
-    };
-
-    /**
      * This class represents an MLE Tx message.
      *
      */
@@ -1079,6 +1045,22 @@
         Error AppendTlvRequestTlv(const uint8_t *aTlvs, uint8_t aTlvsLength);
 
         /**
+         * This method appends a TLV Request TLV to the message.
+         *
+         * @tparam kArrayLength     The TLV array length.
+         *
+         * @param[in]  aTlvArray    A reference to an array of TLV types of @p kArrayLength length.
+         *
+         * @retval kErrorNone     Successfully appended the TLV Request TLV.
+         * @retval kErrorNoBufs   Insufficient buffers available to append the TLV Request TLV.
+         *
+         */
+        template <uint8_t kArrayLength> Error AppendTlvRequestTlv(const uint8_t (&aTlvArray)[kArrayLength])
+        {
+            return AppendTlvRequestTlv(aTlvArray, kArrayLength);
+        }
+
+        /**
          * This method appends a Leader Data TLV to the message.
          *
          * @retval kErrorNone     Successfully appended the Leader Data TLV.
@@ -1140,6 +1122,17 @@
          */
         Error AppendAddressRegistrationTlv(AddressRegistrationMode aMode = kAppendAllAddresses);
 
+        /**
+         * This method appends a Supervision Interval TLV to the message.
+         *
+         * @param[in]  aInterval  The interval value.
+         *
+         * @retval kErrorNone    Successfully appended the Supervision Interval TLV.
+         * @retval kErrorNoBufs  Insufficient buffers available to append the Supervision Interval TLV.
+         *
+         */
+        Error AppendSupervisionIntervalTlv(uint16_t aInterval);
+
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
         /**
          * This method appends a Time Request TLV to the message.
@@ -1265,7 +1258,7 @@
          * @retval kErrorNoBufs   Insufficient buffers available to append the Connectivity TLV.
          *
          */
-        Error AppendAddresseRegisterationTlv(Child &aChild);
+        Error AppendAddressRegistrationTlv(Child &aChild);
 #endif // OPENTHREAD_FTD
 
         /**
@@ -1290,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);
     };
 
     /**
@@ -1344,14 +1341,14 @@
         /**
          * This method reads TLV Request TLV from the message.
          *
-         * @param[out] aRequestedTlvs   A reference to output the read list of requested TLVs.
+         * @param[out] aTlvList     A reference to output the read list of requested TLVs.
          *
          * @retval kErrorNone       Successfully read the TLV.
          * @retval kErrorNotFound   TLV was not found in the message.
          * @retval kErrorParse      TLV was found but could not be parsed.
          *
          */
-        Error ReadTlvRequestTlv(RequestedTlvs &aRequestedTlvs) const;
+        Error ReadTlvRequestTlv(TlvList &aTlvList) const;
 
         /**
          * This method reads Leader Data TLV from a message.
@@ -1365,6 +1362,34 @@
          */
         Error ReadLeaderDataTlv(LeaderData &aLeaderData) const;
 
+#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+        /**
+         * This method reads CSL Clock Accuracy TLV from a message.
+         *
+         * @param[out] aCslAccuracy A reference to output the CSL accuracy.
+         *
+         * @retval kErrorNone       Successfully read the TLV.
+         * @retval kErrorNotFound   TLV was not found in the message.
+         * @retval kErrorParse      TLV was found but could not be parsed.
+         *
+         */
+        Error ReadCslClockAccuracyTlv(Mac::CslAccuracy &aCslAccuracy) const;
+#endif
+
+#if OPENTHREAD_FTD
+        /**
+         * This method reads and validates Route TLV from a message.
+         *
+         * @param[out] aRouteTlv    A reference to output the read Route TLV.
+         *
+         * @retval kErrorNone       Successfully read and validated the Route TLV.
+         * @retval kErrorNotFound   TLV was not found in the message.
+         * @retval kErrorParse      TLV was found but could not be parsed or is not valid.
+         *
+         */
+        Error ReadRouteTlv(RouteTlv &aRouteTlv) const;
+#endif
+
     private:
         Error ReadChallengeOrResponse(uint8_t aTlvType, Challenge &aBuffer) const;
     };
@@ -1404,11 +1429,21 @@
         {
         }
 
-        RxMessage &             mMessage;      ///< The MLE message.
+        /**
+         * This method indicates whether the `mNeighbor` (neighbor from which message was received) is non-null and
+         * in valid state.
+         *
+         * @retval TRUE  If `mNeighbor` is non-null and in valid state.
+         * @retval FALSE If `mNeighbor` is `nullptr` or not in valid state.
+         *
+         */
+        bool IsNeighborStateValid(void) const { return (mNeighbor != nullptr) && mNeighbor->IsStateValid(); }
+
+        RxMessage              &mMessage;      ///< The MLE message.
         const Ip6::MessageInfo &mMessageInfo;  ///< The `MessageInfo` associated with the message.
         uint32_t                mFrameCounter; ///< The frame counter from aux security header.
         uint32_t                mKeySequence;  ///< The key sequence from aux security header.
-        Neighbor *              mNeighbor;     ///< Neighbor from which message was received (can be `nullptr`).
+        Neighbor               *mNeighbor;     ///< Neighbor from which message was received (can be `nullptr`).
         Class                   mClass;        ///< The message class (authoritative, peer, or unknown).
     };
 
@@ -1447,6 +1482,28 @@
     void SetAttachState(AttachState aState);
 
     /**
+     * This method initializes a given @p aNeighbor with information from @p aRxInfo.
+     *
+     * This method updates the following properties on @p aNeighbor from @p aRxInfo:
+     *
+     * - Sets the Extended MAC address from `MessageInfo` peer IPv6 address IID.
+     * - Clears the `GetLinkInfo()` and adds RSS from the `GetThreadLinkInfo()`.
+     * - Resets the link failure counter (`ResetLinkFailures()`).
+     * - Sets the "last heard" time to now (`SetLastHeard()`).
+     *
+     * @param[in,out] aNeighbor   The `Neighbor` to initialize.
+     * @param[in]     aRxInfo     The `RxtInfo` to use for initialization.
+     *
+     */
+    void InitNeighbor(Neighbor &aNeighbor, const RxInfo &aRxInfo);
+
+    /**
+     * This method clears the parent candidate.
+     *
+     */
+    void ClearParentCandidate(void) { mParentCandidate.Clear(); }
+
+    /**
      * This method checks if the destination is reachable.
      *
      * @param[in]  aMeshDest   The RLOC16 of the destination.
@@ -1471,47 +1528,49 @@
     /**
      * This method generates an MLE Data Request message.
      *
-     * @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]  aExtraTlvs        A pointer to extra TLVs.
-     * @param[in]  aExtraTlvsLength  Length of extra TLVs.
+     * @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,
-                          const uint8_t *     aTlvs,
-                          uint8_t             aTlvsLength,
-                          uint16_t            aDelay,
-                          const uint8_t *     aExtraTlvs       = nullptr,
-                          uint8_t             aExtraTlvsLength = 0);
+    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]  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 SendDataRequestForLinkMetricsReport(const Ip6::Address                      &aDestination,
+                                              const LinkMetrics::Initiator::QueryInfo &aQueryInfo);
+#endif
 
     /**
      * 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.
      *
-     * @param[in]  aTlvs         A pointer to requested TLV types.
-     * @param[in]  aNumTlvs      The number of TLV types in @p aTlvs.
+     * @param[in]  aTlvList      A list of requested TLV types.
      * @param[in]  aChallenge    The Challenge for the response.
      *
      * @retval kErrorNone     Successfully generated an MLE Child Update Response message.
      * @retval kErrorNoBufs   Insufficient buffers to generate the MLE Child Update Response message.
      *
      */
-    Error SendChildUpdateResponse(const uint8_t *aTlvs, uint8_t aNumTlvs, const Challenge &aChallenge);
+    Error SendChildUpdateResponse(const TlvList &aTlvList, const Challenge &aChallenge);
 
     /**
      * This method sets the RLOC16 assigned to the Thread interface.
@@ -1653,14 +1712,13 @@
      * This method sends a Link Metrics Management Request message.
      *
      * @param[in]  aDestination  A reference to the IPv6 address of the destination.
-     * @param[in]  aSubTlvs      A pointer to the buffer of the sub-TLVs in the message.
-     * @param[in]  aLength       The overall length of @p aSubTlvs.
+     * @param[in]  aSubTlv       A reference to the sub-TLV to include.
      *
      * @retval kErrorNone     Successfully sent a Link Metrics Management Request.
      * @retval kErrorNoBufs   Insufficient buffers to generate the MLE Link Metrics Management Request message.
      *
      */
-    Error SendLinkMetricsManagementRequest(const Ip6::Address &aDestination, const uint8_t *aSubTlvs, uint8_t aLength);
+    Error SendLinkMetricsManagementRequest(const Ip6::Address &aDestination, const ot::Tlv &aSubTlv);
 
     /**
      * This method sends an MLE Link Probe message.
@@ -1679,51 +1737,45 @@
 
 #endif
 
-    /**
-     * This method indicates whether the device is detaching gracefully.
-     *
-     * @retval TRUE  Detaching is in progress.
-     * @retval FALSE Not detaching.
-     *
-     */
-    bool IsDetachingGracefully(void) { return mDetachGracefullyTimer.IsRunning(); }
+    void ScheduleMessageTransmissionTimer(void);
+
+private:
+    // Declare early so we can use in as `TimerMilli` callbacks.
+    void HandleAttachTimer(void);
+    void HandleDelayedResponseTimer(void);
+    void HandleMessageTransmissionTimer(void);
+
+protected:
+    using AttachTimer = TimerMilliIn<Mle, &Mle::HandleAttachTimer>;
+    using DelayTimer  = TimerMilliIn<Mle, &Mle::HandleDelayedResponseTimer>;
+    using MsgTxTimer  = TimerMilliIn<Mle, &Mle::HandleMessageTransmissionTimer>;
 
     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.
-    Router        mParent;                   ///< Parent information.
-    Router        mParentCandidate;          ///< Parent candidate 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.
-    TimerMilli    mAttachTimer;              ///< The timer for driving the attach process.
-    TimerMilli    mDelayedResponseTimer;     ///< The timer to delay MLE responses.
-    TimerMilli    mMessageTransmissionTimer; ///< The timer for (re-)sending of MLE messages (e.g. Child Update).
-    TimerMilli    mDetachGracefullyTimer;
-    uint8_t       mParentLeaderCost;
-
-    otDetachGracefullyCallback mDetachGracefullyCallback;
-    void *                     mDetachGracefullyContext;
-
-    static constexpr uint32_t kDetachGracefullyTimeout = 1000;
+    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.
+#endif
 
 private:
     static constexpr uint8_t kMleHopLimit        = 255;
     static constexpr uint8_t kMleSecurityTagSize = 4; // Security tag size in bytes.
 
-    // Parameters related to "periodic parent search" feature (CONFIG_ENABLE_PERIODIC_PARENT_SEARCH).
-    // All timer intervals are converted to milliseconds.
-    static constexpr uint32_t kParentSearchCheckInterval   = (OPENTHREAD_CONFIG_PARENT_SEARCH_CHECK_INTERVAL * 1000u);
-    static constexpr uint32_t kParentSearchBackoffInterval = (OPENTHREAD_CONFIG_PARENT_SEARCH_BACKOFF_INTERVAL * 1000u);
-    static constexpr uint32_t kParentSearchJitterInterval  = (15 * 1000u);
-    static constexpr int8_t   kParentSearchRssThreadhold   = OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_THRESHOLD;
-
     // Parameters for "attach backoff" feature (CONFIG_ENABLE_ATTACH_BACKOFF) - Intervals are in milliseconds.
     static constexpr uint32_t kAttachBackoffMinInterval = OPENTHREAD_CONFIG_MLE_ATTACH_BACKOFF_MINIMUM_INTERVAL;
     static constexpr uint32_t kAttachBackoffMaxInterval = OPENTHREAD_CONFIG_MLE_ATTACH_BACKOFF_MAXIMUM_INTERVAL;
@@ -1745,6 +1797,12 @@
     static constexpr uint8_t kNextAttachCycleTotalParentRequests       = 2;
     static constexpr uint8_t kNextAttachCycleNumParentRequestToRouters = 1;
 
+    static constexpr uint32_t kDetachGracefullyTimeout = 1000;
+
+    static constexpr uint32_t kStoreFrameCounterAhead   = OPENTHREAD_CONFIG_STORE_FRAME_COUNTER_AHEAD;
+    static constexpr uint8_t  kMaxIpAddressesToRegister = OPENTHREAD_CONFIG_MLE_IP_ADDRS_TO_REGISTER;
+    static constexpr uint32_t kDefaultCslTimeout        = OPENTHREAD_CONFIG_CSL_TIMEOUT;
+
     enum StartMode : uint8_t // Used in `Start()`.
     {
         kNormalAttach,
@@ -1776,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.
@@ -1816,7 +1881,8 @@
         }
 
     private:
-        static constexpr uint8_t kKeyIdMode2Mic32 = (Mac::Frame::kKeyIdMode2 | Mac::Frame::kSecEncMic32);
+        static constexpr uint8_t kKeyIdMode2Mic32 =
+            static_cast<uint8_t>(Mac::Frame::kKeyIdMode2) | static_cast<uint8_t>(Mac::Frame::kSecurityEncMic32);
 
         uint8_t  mSecurityControl;
         uint32_t mFrameCounter;
@@ -1824,6 +1890,25 @@
         uint8_t  mKeyIndex;
     } OT_TOOL_PACKED_END;
 
+    class ParentCandidate : public Parent
+    {
+    public:
+        void Init(Instance &aInstance) { Parent::Init(aInstance); }
+        void Clear(void);
+        void CopyTo(Parent &aParent) const;
+
+        Challenge  mChallenge;
+        int8_t     mPriority;
+        uint8_t    mLinkQuality3;
+        uint8_t    mLinkQuality2;
+        uint8_t    mLinkQuality1;
+        uint16_t   mSedBufferSize;
+        uint8_t    mSedDatagramCount;
+        uint8_t    mLinkMargin;
+        LeaderData mLeaderData;
+        bool       mIsSingleton;
+    };
+
 #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
     class ServiceAloc : public Ip6::Netif::UnicastAddress
     {
@@ -1840,27 +1925,70 @@
     };
 #endif
 
+#if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
+    void HandleParentSearchTimer(void) { mParentSearch.HandleTimer(); }
+
+    class ParentSearch : public InstanceLocator
+    {
+    public:
+        explicit ParentSearch(Instance &aInstance)
+            : InstanceLocator(aInstance)
+            , mIsInBackoff(false)
+            , mBackoffWasCanceled(false)
+            , mRecentlyDetached(false)
+            , mBackoffCancelTime(0)
+            , mTimer(aInstance)
+        {
+        }
+
+        void StartTimer(void);
+        void UpdateState(void);
+        void SetRecentlyDetached(void) { mRecentlyDetached = true; }
+        void HandleTimer(void);
+
+    private:
+        // All timer intervals are converted to milliseconds.
+        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   kRssThreshold    = OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_THRESHOLD;
+
+        using SearchTimer = TimerMilliIn<Mle, &Mle::HandleParentSearchTimer>;
+
+        bool        mIsInBackoff : 1;
+        bool        mBackoffWasCanceled : 1;
+        bool        mRecentlyDetached : 1;
+        TimeMilli   mBackoffCancelTime;
+        SearchTimer mTimer;
+    };
+#endif // OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
+
     Error       Start(StartMode aMode);
     void        Stop(StopMode aMode);
     void        HandleNotifierEvents(Events aEvents);
-    static void HandleAttachTimer(Timer &aTimer);
-    void        HandleAttachTimer(void);
-    static void HandleDelayedResponseTimer(Timer &aTimer);
-    void        HandleDelayedResponseTimer(void);
     void        SendDelayedResponse(TxMessage &aMessage, const DelayedResponseMetadata &aMetadata);
-    static void HandleMessageTransmissionTimer(Timer &aTimer);
-    void        HandleMessageTransmissionTimer(void);
     static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
     void        HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    void        ScheduleMessageTransmissionTimer(void);
     void        ReestablishLinkWithNeighbor(Neighbor &aNeighbor);
     static void HandleDetachGracefullyTimer(Timer &aTimer);
     void        HandleDetachGracefullyTimer(void);
-    Error       SendChildUpdateRequest(bool aAppendChallenge, uint32_t aTimeout);
+    bool        IsDetachingGracefully(void) { return mDetachGracefullyTimer.IsRunning(); }
+    Error       SendChildUpdateRequest(ChildUpdateRequestMode aMode);
+    Error       SendDataRequestAfterDelay(const Ip6::Address &aDestination, uint16_t aDelay);
+
+#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
 
 #if OPENTHREAD_FTD
-    static void HandleDetachGracefullyAddressReleaseResponse(void *               aContext,
-                                                             otMessage *          aMessage,
+    static void HandleDetachGracefullyAddressReleaseResponse(void                *aContext,
+                                                             otMessage           *aMessage,
                                                              const otMessageInfo *aMessageInfo,
                                                              Error                aResult);
     void        HandleDetachGracefullyAddressReleaseResponse(void);
@@ -1875,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);
@@ -1889,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);
@@ -1903,20 +2029,19 @@
     bool     HasAcceptableParentCandidate(void) const;
     Error    DetermineParentRequestType(ParentRequestType &aType) const;
 
-    bool IsBetterParent(uint16_t               aRloc16,
-                        LinkQuality            aLinkQuality,
-                        uint8_t                aLinkMargin,
-                        const ConnectivityTlv &aConnectivityTlv,
-                        uint8_t                aVersion,
-                        uint8_t                aCslClockAccuracy,
-                        uint8_t                aCslUncertainty);
+    bool IsBetterParent(uint16_t                aRloc16,
+                        LinkQuality             aLinkQuality,
+                        uint8_t                 aLinkMargin,
+                        const ConnectivityTlv  &aConnectivityTlv,
+                        uint16_t                aVersion,
+                        const Mac::CslAccuracy &aCslAccuracy);
     bool IsNetworkDataNewer(const LeaderData &aLeaderData);
 
     Error ProcessMessageSecurity(Crypto::AesCcm::Mode    aMode,
-                                 Message &               aMessage,
+                                 Message                &aMessage,
                                  const Ip6::MessageInfo &aMessageInfo,
                                  uint16_t                aCmdOffset,
-                                 const SecurityHeader &  aHeader);
+                                 const SecurityHeader   &aHeader);
 
 #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
     ServiceAloc *FindInServiceAlocs(uint16_t aAloc16);
@@ -1927,13 +2052,6 @@
     void InformPreviousParent(void);
 #endif
 
-#if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
-    static void HandleParentSearchTimer(Timer &aTimer);
-    void        HandleParentSearchTimer(void);
-    void        StartParentSearchTimer(void);
-    void        UpdateParentSearchState(void);
-#endif
-
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
     static void        LogError(MessageAction aAction, MessageType aType, Error aError);
     static const char *MessageActionToString(MessageAction aAction);
@@ -1941,17 +2059,18 @@
     static const char *MessageTypeActionToSuffixString(MessageType aType, MessageAction aAction);
 #endif
 
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    void UpdateRoleTimeCounters(DeviceRole aRole);
+#endif
+
+    using DetachGracefullyTimer = TimerMilliIn<Mle, &Mle::HandleDetachGracefullyTimer>;
+
     MessageQueue mDelayedResponses;
 
     Challenge mParentRequestChallenge;
 
-    AttachMode mAttachMode;
-    int8_t     mParentPriority;
-    uint8_t    mParentLinkQuality3;
-    uint8_t    mParentLinkQuality2;
-    uint8_t    mParentLinkQuality1;
-    uint16_t   mParentSedBufferSize;
-    uint8_t    mParentSedDatagramCount;
+    AttachMode      mAttachMode;
+    ParentCandidate mParentCandidate;
 
     uint8_t                 mChildUpdateAttempts;
     ChildUpdateRequestState mChildUpdateRequestState;
@@ -1960,13 +2079,9 @@
 
     AddressRegistrationMode mAddressRegistrationMode;
 
-    bool       mHasRestored;
-    uint8_t    mParentLinkMargin;
-    bool       mParentIsSingleton;
-    bool       mReceivedResponseFromParent;
-    LeaderData mParentLeaderData;
-
-    Challenge mParentCandidateChallenge;
+    bool mHasRestored;
+    bool mReceivedResponseFromParent;
+    bool mInitiallyAttachedAsSleepy;
 
     Ip6::Udp::Socket mSocket;
     uint32_t         mTimeout;
@@ -1974,14 +2089,11 @@
     uint32_t mCslTimeout;
 #endif
 
+    uint16_t mRloc16;
     uint16_t mPreviousParentRloc;
 
 #if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
-    bool       mParentSearchIsInBackoff : 1;
-    bool       mParentSearchBackoffWasCanceled : 1;
-    bool       mParentSearchRecentlyDetached : 1;
-    TimeMilli  mParentSearchBackoffCancelTime;
-    TimerMilli mParentSearchTimer;
+    ParentSearch mParentSearch;
 #endif
 
     uint8_t  mAnnounceChannel;
@@ -1993,7 +2105,10 @@
     ServiceAloc mServiceAlocs[kMaxServiceAlocs];
 #endif
 
-    otMleCounters mCounters;
+    Counters mCounters;
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    uint64_t mLastUpdatedTimestamp;
+#endif
 
     static const otMeshLocalPrefix sMeshLocalPrefixInit;
 
@@ -2003,8 +2118,12 @@
     Ip6::Netif::MulticastAddress mLinkLocalAllThreadNodes;
     Ip6::Netif::MulticastAddress mRealmLocalAllThreadNodes;
 
-    otThreadParentResponseCallback mParentResponseCb;
-    void *                         mParentResponseCbContext;
+    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 d630347..bade0cb 100644
--- a/src/core/thread/mle_router.cpp
+++ b/src/core/thread/mle_router.cpp
@@ -40,6 +40,7 @@
 #include "common/encoding.hpp"
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 #include "common/serial_number.hpp"
 #include "common/settings.hpp"
@@ -51,6 +52,7 @@
 #include "thread/thread_tlvs.hpp"
 #include "thread/time_sync_service.hpp"
 #include "thread/uri_paths.hpp"
+#include "thread/version.hpp"
 #include "utils/otns.hpp"
 
 namespace ot {
@@ -61,8 +63,6 @@
 MleRouter::MleRouter(Instance &aInstance)
     : Mle(aInstance)
     , mAdvertiseTrickleTimer(aInstance, MleRouter::HandleAdvertiseTrickleTimer)
-    , mAddressSolicit(UriPath::kAddressSolicit, &MleRouter::HandleAddressSolicit, this)
-    , mAddressRelease(UriPath::kAddressRelease, &MleRouter::HandleAddressRelease, this)
     , mChildTable(aInstance)
     , mRouterTable(aInstance)
     , mChallengeTimeout(0)
@@ -70,7 +70,6 @@
     , mNetworkIdTimeout(kNetworkIdTimeout)
     , mRouterUpgradeThreshold(kRouterUpgradeThreshold)
     , mRouterDowngradeThreshold(kRouterDowngradeThreshold)
-    , mLeaderWeight(kLeaderWeight)
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     , mPreferredLeaderPartitionId(0)
     , mCcmEnabled(false)
@@ -85,7 +84,7 @@
     , mPreviousPartitionIdTimeout(0)
     , mRouterSelectionJitter(kRouterSelectionJitter)
     , mRouterSelectionJitterTimeout(0)
-    , mLinkRequestDelay(0)
+    , mChildRouterLinks(kChildRouterLinks)
     , mParentPriority(kParentPriorityUnspecified)
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
     , mBackboneRouterRegistrationDelay(0)
@@ -93,10 +92,9 @@
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     , mMaxChildIpAddresses(0)
 #endif
-    , mDiscoveryRequestCallback(nullptr)
-    , mDiscoveryRequestCallbackContext(nullptr)
 {
     mDeviceMode.Set(mDeviceMode.Get() | DeviceMode::kModeFullThreadDevice | DeviceMode::kModeFullNetworkData);
+    mLeaderWeight = mDeviceProperties.CalculateLeaderWeight();
 
     SetRouterId(kInvalidRouterId);
 
@@ -185,6 +183,13 @@
     return error;
 }
 
+void MleRouter::SetDeviceProperties(const DeviceProperties &aDeviceProperties)
+{
+    mDeviceProperties = aDeviceProperties;
+    mDeviceProperties.ClampWeightAdjustment();
+    SetLeaderWeight(mDeviceProperties.CalculateLeaderWeight());
+}
+
 Error MleRouter::BecomeRouter(ThreadStatusTlv::Status aStatus)
 {
     Error error = kErrorNone;
@@ -197,12 +202,21 @@
 
     Get<MeshForwarder>().SetRxOnWhenIdle(true);
     mRouterSelectionJitterTimeout = 0;
-    mLinkRequestDelay             = 0;
 
     switch (mRole)
     {
     case kRoleDetached:
+        // If router had more than `kMinCriticalChildrenCount` children
+        // or was a leader prior to reset we treat the multicast Link
+        // Request as a critical message.
+        mLinkRequestAttempts =
+            (mWasLeader || mChildTable.GetNumChildren(Child::kInStateValidOrRestoring) >= kMinCriticalChildrenCount)
+                ? kMaxCriticalTransmissionCount
+                : kMaxTransmissionCount;
+
         SuccessOrExit(error = SendLinkRequest(nullptr));
+        mLinkRequestAttempts--;
+        ScheduleMessageTransmissionTimer();
         Get<TimeTicker>().RegisterReceiver(TimeTicker::kMleRouter);
         break;
 
@@ -212,7 +226,6 @@
 
     default:
         OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
     }
 
 exit:
@@ -222,13 +235,9 @@
 Error MleRouter::BecomeLeader(void)
 {
     Error    error = kErrorNone;
-    Router * router;
+    Router  *router;
     uint32_t partitionId;
     uint8_t  leaderId;
-#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-    uint8_t minRouterId;
-    uint8_t maxRouterId;
-#endif
 
     VerifyOrExit(!Get<MeshCoP::ActiveDatasetManager>().IsPartiallyComplete(), error = kErrorInvalidState);
     VerifyOrExit(!IsDisabled(), error = kErrorInvalidState);
@@ -238,24 +247,20 @@
     mRouterTable.Clear();
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-    partitionId = mPreferredLeaderPartitionId ? mPreferredLeaderPartitionId : Random::NonCrypto::GetUint32();
+    {
+        uint8_t minId;
+        uint8_t maxId;
+
+        mRouterTable.GetRouterIdRange(minId, maxId);
+        partitionId = mPreferredLeaderPartitionId ? mPreferredLeaderPartitionId : Random::NonCrypto::GetUint32();
+        leaderId    = (IsRouterIdValid(mPreviousRouterId) && minId <= mPreviousRouterId && mPreviousRouterId <= maxId)
+                          ? mPreviousRouterId
+                          : Random::NonCrypto::GetUint8InRange(minId, maxId + 1);
+    }
 #else
     partitionId = Random::NonCrypto::GetUint32();
-#endif
-
-#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-    mRouterTable.GetRouterIdRange(minRouterId, maxRouterId);
-    if (IsRouterIdValid(mPreviousRouterId) && minRouterId <= mPreviousRouterId && mPreviousRouterId <= maxRouterId)
-    {
-        leaderId = mPreviousRouterId;
-    }
-    else
-    {
-        leaderId = Random::NonCrypto::GetUint8InRange(minRouterId, maxRouterId + 1);
-    }
-#else
     leaderId    = IsRouterIdValid(mPreviousRouterId) ? mPreviousRouterId
-                                                  : Random::NonCrypto::GetUint8InRange(0, kMaxRouterId + 1);
+                                                     : Random::NonCrypto::GetUint8InRange(0, kMaxRouterId + 1);
 #endif
 
     SetLeaderData(partitionId, mLeaderWeight, leaderId);
@@ -277,12 +282,7 @@
 
 void MleRouter::StopLeader(void)
 {
-    Get<Tmf::Agent>().RemoveResource(mAddressSolicit);
-    Get<Tmf::Agent>().RemoveResource(mAddressRelease);
-    Get<MeshCoP::ActiveDatasetManager>().StopLeader();
-    Get<MeshCoP::PendingDatasetManager>().StopLeader();
     StopAdvertiseTrickleTimer();
-    Get<NetworkData::Leader>().Stop();
     Get<ThreadNetif>().UnsubscribeAllRoutersMulticast();
 }
 
@@ -295,7 +295,6 @@
 
 void MleRouter::HandleChildStart(AttachMode aMode)
 {
-    mLinkRequestDelay       = 0;
     mAddressSolicitRejected = false;
 
     mRouterSelectionJitterTimeout = 1 + Random::NonCrypto::GetUint8InRange(0, mRouterSelectionJitter);
@@ -317,13 +316,11 @@
     case kDowngradeToReed:
         SendAddressRelease();
 
-        // reset children info if any
         if (HasChildren())
         {
             RemoveChildren();
         }
 
-        // reset routerId info
         SetRouterId(kInvalidRouterId);
         break;
 
@@ -374,68 +371,50 @@
 
 void MleRouter::SetStateRouter(uint16_t aRloc16)
 {
-    SetRloc16(aRloc16);
-
-    SetRole(kRoleRouter);
-    SetAttachState(kAttachStateIdle);
-    mAttachCounter = 0;
-    mAttachTimer.Stop();
-    mMessageTransmissionTimer.Stop();
-    StopAdvertiseTrickleTimer();
-    ResetAdvertiseInterval();
-
-    Get<ThreadNetif>().SubscribeAllRoutersMulticast();
-    mPreviousPartitionIdRouter = mLeaderData.GetPartitionId();
-    Get<NetworkData::Leader>().Stop();
-    Get<Ip6::Ip6>().SetForwardingEnabled(true);
-    Get<Ip6::Mpl>().SetTimerExpirations(kMplRouterDataMessageTimerExpirations);
-    Get<Mac::Mac>().SetBeaconEnabled(true);
-
-    // remove children that do not have matching RLOC16
-    for (Child &child : Get<ChildTable>().Iterate(Child::kInStateValidOrRestoring))
-    {
-        if (RouterIdFromRloc16(child.GetRloc16()) != mRouterId)
-        {
-            RemoveNeighbor(child);
-        }
-    }
-
-#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    Get<Mac::Mac>().UpdateCsl();
-#endif
+    // The `aStartMode` is ignored when used with `kRoleRouter`
+    SetStateRouterOrLeader(kRoleRouter, aRloc16, /* aStartMode */ kStartingAsLeader);
 }
 
 void MleRouter::SetStateLeader(uint16_t aRloc16, LeaderStartMode aStartMode)
 {
-    IgnoreError(Get<MeshCoP::ActiveDatasetManager>().Restore());
-    IgnoreError(Get<MeshCoP::PendingDatasetManager>().Restore());
+    SetStateRouterOrLeader(kRoleLeader, aRloc16, aStartMode);
+}
+
+void MleRouter::SetStateRouterOrLeader(DeviceRole aRole, uint16_t aRloc16, LeaderStartMode aStartMode)
+{
+    if (aRole == kRoleLeader)
+    {
+        IgnoreError(Get<MeshCoP::ActiveDatasetManager>().Restore());
+        IgnoreError(Get<MeshCoP::PendingDatasetManager>().Restore());
+    }
+
     SetRloc16(aRloc16);
 
-    SetRole(kRoleLeader);
+    SetRole(aRole);
+
     SetAttachState(kAttachStateIdle);
     mAttachCounter = 0;
     mAttachTimer.Stop();
     mMessageTransmissionTimer.Stop();
     StopAdvertiseTrickleTimer();
     ResetAdvertiseInterval();
-    IgnoreError(GetLeaderAloc(mLeaderAloc.GetAddress()));
-    Get<ThreadNetif>().AddUnicastAddress(mLeaderAloc);
 
     Get<ThreadNetif>().SubscribeAllRoutersMulticast();
     mPreviousPartitionIdRouter = mLeaderData.GetPartitionId();
-    Get<TimeTicker>().RegisterReceiver(TimeTicker::kMleRouter);
-
-    Get<NetworkData::Leader>().Start(aStartMode);
-    Get<MeshCoP::ActiveDatasetManager>().StartLeader();
-    Get<MeshCoP::PendingDatasetManager>().StartLeader();
-    Get<Tmf::Agent>().AddResource(mAddressSolicit);
-    Get<Tmf::Agent>().AddResource(mAddressRelease);
-    Get<Ip6::Ip6>().SetForwardingEnabled(true);
-    Get<Ip6::Mpl>().SetTimerExpirations(kMplRouterDataMessageTimerExpirations);
     Get<Mac::Mac>().SetBeaconEnabled(true);
-    Get<AddressResolver>().Clear();
 
-    // remove children that do not have matching RLOC16
+    if (aRole == kRoleLeader)
+    {
+        IgnoreError(GetLeaderAloc(mLeaderAloc.GetAddress()));
+        Get<ThreadNetif>().AddUnicastAddress(mLeaderAloc);
+        Get<TimeTicker>().RegisterReceiver(TimeTicker::kMleRouter);
+        Get<NetworkData::Leader>().Start(aStartMode);
+        Get<MeshCoP::ActiveDatasetManager>().StartLeader();
+        Get<MeshCoP::PendingDatasetManager>().StartLeader();
+        Get<AddressResolver>().Clear();
+    }
+
+    // Remove children that do not have matching RLOC16
     for (Child &child : Get<ChildTable>().Iterate(Child::kInStateValidOrRestoring))
     {
         if (RouterIdFromRloc16(child.GetRloc16()) != mRouterId)
@@ -448,7 +427,7 @@
     Get<Mac::Mac>().UpdateCsl();
 #endif
 
-    LogNote("Leader partition id 0x%x", mLeaderData.GetPartitionId());
+    LogNote("Partition ID 0x%lx", ToUlong(mLeaderData.GetPartitionId()));
 }
 
 void MleRouter::HandleAdvertiseTrickleTimer(TrickleTimer &aTimer)
@@ -466,10 +445,7 @@
     return;
 }
 
-void MleRouter::StopAdvertiseTrickleTimer(void)
-{
-    mAdvertiseTrickleTimer.Stop();
-}
+void MleRouter::StopAdvertiseTrickleTimer(void) { mAdvertiseTrickleTimer.Stop(); }
 
 void MleRouter::ResetAdvertiseInterval(void)
 {
@@ -491,7 +467,7 @@
 {
     Error        error = kErrorNone;
     Ip6::Address destination;
-    TxMessage *  message = nullptr;
+    TxMessage   *message = nullptr;
 
     // Suppress MLE Advertisements when trying to attach to a better partition.
     //
@@ -506,22 +482,12 @@
     // children to detach.
     VerifyOrExit(!mAddressSolicitPending);
 
-    // Suppress MLE Advertisements before sending multicast Link Request.
-    //
-    // Before sending the multicast Link Request message, no links have been established to neighboring routers.
-    VerifyOrExit(mLinkRequestDelay == 0);
-
     VerifyOrExit((message = NewMleMessage(kCommandAdvertisement)) != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = message->AppendSourceAddressTlv());
     SuccessOrExit(error = message->AppendLeaderDataTlv());
 
     switch (mRole)
     {
-    case kRoleDisabled:
-    case kRoleDetached:
-        OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
-
     case kRoleChild:
         break;
 
@@ -529,6 +495,10 @@
     case kRoleLeader:
         SuccessOrExit(error = message->AppendRouteTlv());
         break;
+
+    case kRoleDisabled:
+    case kRoleDetached:
+        OT_ASSERT(false);
     }
 
     destination.SetToLinkLocalAllNodesMulticast();
@@ -543,14 +513,15 @@
 
 Error MleRouter::SendLinkRequest(Neighbor *aNeighbor)
 {
-    static const uint8_t detachedTlvs[]      = {Tlv::kAddress16, Tlv::kRoute};
-    static const uint8_t routerTlvs[]        = {Tlv::kLinkMargin};
-    static const uint8_t validNeighborTlvs[] = {Tlv::kLinkMargin, Tlv::kRoute};
-    Error                error               = kErrorNone;
-    TxMessage *          message             = nullptr;
-    Ip6::Address         destination;
+    static const uint8_t kDetachedTlvs[]      = {Tlv::kAddress16, Tlv::kRoute};
+    static const uint8_t kRouterTlvs[]        = {Tlv::kLinkMargin};
+    static const uint8_t kValidNeighborTlvs[] = {Tlv::kLinkMargin, Tlv::kRoute};
 
-    VerifyOrExit(mLinkRequestDelay == 0 && mChallengeTimeout == 0);
+    Error        error   = kErrorNone;
+    TxMessage   *message = nullptr;
+    Ip6::Address destination;
+
+    VerifyOrExit(mChallengeTimeout == 0);
 
     destination.Clear();
 
@@ -559,12 +530,8 @@
 
     switch (mRole)
     {
-    case kRoleDisabled:
-        OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
-
     case kRoleDetached:
-        SuccessOrExit(error = message->AppendTlvRequestTlv(detachedTlvs, sizeof(detachedTlvs)));
+        SuccessOrExit(error = message->AppendTlvRequestTlv(kDetachedTlvs));
         break;
 
     case kRoleChild:
@@ -576,16 +543,19 @@
     case kRoleLeader:
         if (aNeighbor == nullptr || !aNeighbor->IsStateValid())
         {
-            SuccessOrExit(error = message->AppendTlvRequestTlv(routerTlvs, sizeof(routerTlvs)));
+            SuccessOrExit(error = message->AppendTlvRequestTlv(kRouterTlvs));
         }
         else
         {
-            SuccessOrExit(error = message->AppendTlvRequestTlv(validNeighborTlvs, sizeof(validNeighborTlvs)));
+            SuccessOrExit(error = message->AppendTlvRequestTlv(kValidNeighborTlvs));
         }
 
         SuccessOrExit(error = message->AppendSourceAddressTlv());
         SuccessOrExit(error = message->AppendLeaderDataTlv());
         break;
+
+    case kRoleDisabled:
+        OT_ASSERT(false);
     }
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
@@ -595,7 +565,7 @@
     if (aNeighbor == nullptr)
     {
         mChallenge.GenerateRandom();
-        mChallengeTimeout = (((2 * kMaxResponseDelay) + kStateUpdatePeriod - 1) / kStateUpdatePeriod);
+        mChallengeTimeout = kChallengeTimeout;
 
         SuccessOrExit(error = message->AppendChallengeTlv(mChallenge));
         destination.SetToLinkLocalAllRoutersMulticast();
@@ -630,13 +600,13 @@
 
 void MleRouter::HandleLinkRequest(RxInfo &aRxInfo)
 {
-    Error         error    = kErrorNone;
-    Neighbor *    neighbor = nullptr;
-    Challenge     challenge;
-    uint16_t      version;
-    LeaderData    leaderData;
-    uint16_t      sourceAddress;
-    RequestedTlvs requestedTlvs;
+    Error      error    = kErrorNone;
+    Neighbor  *neighbor = nullptr;
+    Challenge  challenge;
+    uint16_t   version;
+    LeaderData leaderData;
+    uint16_t   sourceAddress;
+    TlvList    requestedTlvList;
 
     Log(kMessageReceive, kTypeLinkRequest, aRxInfo.mMessageInfo.GetPeerAddr());
 
@@ -649,7 +619,7 @@
 
     // Version
     SuccessOrExit(error = Tlv::Find<VersionTlv>(aRxInfo.mMessage, version));
-    VerifyOrExit(version >= OT_THREAD_VERSION_1_1, error = kErrorParse);
+    VerifyOrExit(version >= kThreadVersion1p1, error = kErrorParse);
 
     // Leader Data
     switch (aRxInfo.mMessage.ReadLeaderDataTlv(leaderData))
@@ -669,25 +639,20 @@
     case kErrorNone:
         if (IsActiveRouter(sourceAddress))
         {
-            Mac::ExtAddress extAddr;
-
-            aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(extAddr);
-
-            neighbor = mRouterTable.GetRouter(RouterIdFromRloc16(sourceAddress));
+            neighbor = mRouterTable.FindRouterByRloc16(sourceAddress);
             VerifyOrExit(neighbor != nullptr, error = kErrorParse);
             VerifyOrExit(!neighbor->IsStateLinkRequest(), error = kErrorAlready);
 
             if (!neighbor->IsStateValid())
             {
-                neighbor->SetExtAddress(extAddr);
-                neighbor->GetLinkInfo().Clear();
-                neighbor->GetLinkInfo().AddRss(aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss());
-                neighbor->ResetLinkFailures();
-                neighbor->SetLastHeard(TimerMilli::GetNow());
+                InitNeighbor(*neighbor, aRxInfo);
                 neighbor->SetState(Neighbor::kStateLinkRequest);
             }
             else
             {
+                Mac::ExtAddress extAddr;
+
+                aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(extAddr);
                 VerifyOrExit(neighbor->GetExtAddress() == extAddr);
             }
         }
@@ -696,8 +661,7 @@
 
     case kErrorNotFound:
         // lack of source address indicates router coming out of reset
-        VerifyOrExit(aRxInfo.mNeighbor && aRxInfo.mNeighbor->IsStateValid() &&
-                         IsActiveRouter(aRxInfo.mNeighbor->GetRloc16()),
+        VerifyOrExit(aRxInfo.IsNeighborStateValid() && IsActiveRouter(aRxInfo.mNeighbor->GetRloc16()),
                      error = kErrorDrop);
         neighbor = aRxInfo.mNeighbor;
         break;
@@ -707,12 +671,10 @@
     }
 
     // TLV Request
-    switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvs))
+    switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
     {
     case kErrorNone:
-        break;
     case kErrorNotFound:
-        requestedTlvs.mNumTlvs = 0;
         break;
     default:
         ExitNow(error = kErrorParse);
@@ -734,22 +696,23 @@
 
     aRxInfo.mClass = RxInfo::kPeerMessage;
 
-    SuccessOrExit(error = SendLinkAccept(aRxInfo.mMessageInfo, neighbor, requestedTlvs, challenge));
+    SuccessOrExit(error = SendLinkAccept(aRxInfo.mMessageInfo, neighbor, requestedTlvList, challenge));
 
 exit:
     LogProcessError(kTypeLinkRequest, error);
 }
 
 Error MleRouter::SendLinkAccept(const Ip6::MessageInfo &aMessageInfo,
-                                Neighbor *              aNeighbor,
-                                const RequestedTlvs &   aRequestedTlvs,
-                                const Challenge &       aChallenge)
+                                Neighbor               *aNeighbor,
+                                const TlvList          &aRequestedTlvList,
+                                const Challenge        &aChallenge)
 {
-    Error                error        = kErrorNone;
-    static const uint8_t routerTlvs[] = {Tlv::kLinkMargin};
-    TxMessage *          message;
-    Command              command;
-    uint8_t              linkMargin;
+    static const uint8_t kRouterTlvs[] = {Tlv::kLinkMargin};
+
+    Error      error = kErrorNone;
+    TxMessage *message;
+    Command    command;
+    uint8_t    linkMargin;
 
     command = (aNeighbor == nullptr || aNeighbor->IsStateValid()) ? kCommandLinkAccept : kCommandLinkAcceptAndRequest;
 
@@ -761,8 +724,7 @@
     SuccessOrExit(error = message->AppendMleFrameCounterTlv());
 
     // always append a link margin, regardless of whether or not it was requested
-    linkMargin = LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(),
-                                                         aMessageInfo.GetThreadLinkInfo()->GetRss());
+    linkMargin = Get<Mac::Mac>().ComputeLinkMargin(aMessageInfo.GetThreadLinkInfo()->GetRss());
 
     SuccessOrExit(error = message->AppendLinkMarginTlv(linkMargin));
 
@@ -771,9 +733,9 @@
         SuccessOrExit(error = message->AppendLeaderDataTlv());
     }
 
-    for (uint8_t i = 0; i < aRequestedTlvs.mNumTlvs; i++)
+    for (uint8_t tlvType : aRequestedTlvList)
     {
-        switch (aRequestedTlvs.mTlvs[i])
+        switch (tlvType)
         {
         case Tlv::kRoute:
             SuccessOrExit(error = message->AppendRouteTlv(aNeighbor));
@@ -797,7 +759,7 @@
         aNeighbor->GenerateChallenge();
 
         SuccessOrExit(error = message->AppendChallengeTlv(aNeighbor->GetChallenge(), aNeighbor->GetChallengeSize()));
-        SuccessOrExit(error = message->AppendTlvRequestTlv(routerTlvs, sizeof(routerTlvs)));
+        SuccessOrExit(error = message->AppendTlvRequestTlv(kRouterTlvs));
         aNeighbor->SetLastHeard(TimerMilli::GetNow());
         aNeighbor->SetState(Neighbor::kStateLinkRequest);
     }
@@ -814,13 +776,15 @@
         SuccessOrExit(error = message->SendAfterDelay(aMessageInfo.GetPeerAddr(),
                                                       1 + Random::NonCrypto::GetUint16InRange(0, kMaxResponseDelay)));
 
-        Log(kMessageDelay, kTypeLinkAccept, aMessageInfo.GetPeerAddr());
+        Log(kMessageDelay, (command == kCommandLinkAccept) ? kTypeLinkAccept : kTypeLinkAcceptAndRequest,
+            aMessageInfo.GetPeerAddr());
     }
     else
     {
         SuccessOrExit(error = message->SendTo(aMessageInfo.GetPeerAddr()));
 
-        Log(kMessageSend, kTypeLinkAccept, aMessageInfo.GetPeerAddr());
+        Log(kMessageSend, (command == kCommandLinkAccept) ? kTypeLinkAccept : kTypeLinkAcceptAndRequest,
+            aMessageInfo.GetPeerAddr());
     }
 
 exit:
@@ -844,12 +808,9 @@
 
 Error MleRouter::HandleLinkAccept(RxInfo &aRxInfo, bool aRequest)
 {
-    static const uint8_t dataRequestTlvs[] = {Tlv::kNetworkData};
-
     Error           error = kErrorNone;
-    Router *        router;
+    Router         *router;
     Neighbor::State neighborState;
-    Mac::ExtAddress extAddr;
     uint16_t        version;
     Challenge       response;
     uint16_t        sourceAddress;
@@ -870,7 +831,7 @@
     VerifyOrExit(IsActiveRouter(sourceAddress), error = kErrorParse);
 
     routerId      = RouterIdFromRloc16(sourceAddress);
-    router        = mRouterTable.GetRouter(routerId);
+    router        = mRouterTable.FindRouterById(routerId);
     neighborState = (router != nullptr) ? router->GetState() : Neighbor::kStateInvalid;
 
     // Response
@@ -884,7 +845,8 @@
         break;
 
     case Neighbor::kStateInvalid:
-        VerifyOrExit((mChallengeTimeout > 0) && (response == mChallenge), error = kErrorSecurity);
+        VerifyOrExit((mLinkRequestAttempts > 0 || mChallengeTimeout > 0) && (response == mChallenge),
+                     error = kErrorSecurity);
 
         OT_FALL_THROUGH;
 
@@ -903,7 +865,7 @@
 
     // Version
     SuccessOrExit(error = Tlv::Find<VersionTlv>(aRxInfo.mMessage, version));
-    VerifyOrExit(version >= OT_THREAD_VERSION_1_1, error = kErrorParse);
+    VerifyOrExit(version >= kThreadVersion1p1, error = kErrorParse);
 
     // Link and MLE Frame Counters
     SuccessOrExit(error = aRxInfo.mMessage.ReadFrameCounterTlvs(linkFrameCounter, mleFrameCounter));
@@ -925,10 +887,6 @@
 
     switch (mRole)
     {
-    case kRoleDisabled:
-        OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
-
     case kRoleDetached:
         // Address16
         SuccessOrExit(error = Tlv::Find<Address16Tlv>(aRxInfo.mMessage, address16));
@@ -940,8 +898,9 @@
 
         // Route
         mRouterTable.Clear();
-        SuccessOrExit(error = ProcessRouteTlv(aRxInfo));
-        router = mRouterTable.GetRouter(routerId);
+        SuccessOrExit(error = aRxInfo.mMessage.ReadRouteTlv(routeTlv));
+        SuccessOrExit(error = ProcessRouteTlv(routeTlv, aRxInfo));
+        router = mRouterTable.FindRouterById(routerId);
         VerifyOrExit(router != nullptr);
 
         if (mLeaderData.GetLeaderRouterId() == RouterIdFromRloc16(GetRloc16()))
@@ -953,8 +912,9 @@
             SetStateRouter(GetRloc16());
         }
 
+        mLinkRequestAttempts    = 0; // completed router sync after reset, no more link request to retransmit
         mRetrieveNewNetworkData = true;
-        IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr(), dataRequestTlvs, sizeof(dataRequestTlvs), 0));
+        IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr()));
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
         Get<TimeSync>().HandleTimeSyncMessage(aRxInfo.mMessage);
@@ -977,52 +937,53 @@
             SerialNumber::IsGreater(leaderData.GetDataVersion(NetworkData::kFullSet),
                                     Get<NetworkData::Leader>().GetVersion(NetworkData::kFullSet)))
         {
-            IgnoreError(
-                SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr(), dataRequestTlvs, sizeof(dataRequestTlvs), 0));
+            IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr()));
         }
 
         // Route (optional)
-        switch (error = ProcessRouteTlv(aRxInfo, routeTlv))
+        switch (aRxInfo.mMessage.ReadRouteTlv(routeTlv))
         {
         case kErrorNone:
-            UpdateRoutes(routeTlv, routerId);
-            // Need to update router after ProcessRouteTlv
-            router = mRouterTable.GetRouter(routerId);
-            OT_ASSERT(router != nullptr);
+            VerifyOrExit(routeTlv.IsRouterIdSet(routerId), error = kErrorParse);
+
+            if (mRouterTable.IsRouteTlvIdSequenceMoreRecent(routeTlv))
+            {
+                SuccessOrExit(error = ProcessRouteTlv(routeTlv, aRxInfo));
+                router = mRouterTable.FindRouterById(routerId);
+                OT_ASSERT(router != nullptr);
+            }
+
+            mRouterTable.UpdateRoutes(routeTlv, routerId);
             break;
 
         case kErrorNotFound:
-            error = kErrorNone;
             break;
 
         default:
-            ExitNow();
+            ExitNow(error = kErrorParse);
         }
 
-        // update routing table
         if (routerId != mRouterId && !IsRouterIdValid(router->GetNextHop()))
         {
             ResetAdvertiseInterval();
         }
 
         break;
+
+    case kRoleDisabled:
+        OT_ASSERT(false);
     }
 
     // finish link synchronization
-    aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(extAddr);
-    router->SetExtAddress(extAddr);
+    InitNeighbor(*router, aRxInfo);
     router->SetRloc16(sourceAddress);
     router->GetLinkFrameCounters().SetAll(linkFrameCounter);
     router->SetLinkAckFrameCounter(linkFrameCounter);
     router->SetMleFrameCounter(mleFrameCounter);
-    router->SetLastHeard(TimerMilli::GetNow());
-    router->SetVersion(static_cast<uint8_t>(version));
+    router->SetVersion(version);
     router->SetDeviceMode(DeviceMode(DeviceMode::kModeFullThreadDevice | DeviceMode::kModeRxOnWhenIdle |
                                      DeviceMode::kModeFullNetworkData));
-    router->GetLinkInfo().Clear();
-    router->GetLinkInfo().AddRss(aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss());
-    router->SetLinkQualityOut(LinkQualityInfo::ConvertLinkMarginToLinkQuality(linkMargin));
-    router->ResetLinkFailures();
+    router->SetLinkQualityOut(LinkQualityForLinkMargin(linkMargin));
     router->SetState(Neighbor::kStateValid);
     router->SetKeySequence(aRxInfo.mKeySequence);
 
@@ -1032,73 +993,29 @@
 
     if (aRequest)
     {
-        Challenge     challenge;
-        RequestedTlvs requestedTlvs;
+        Challenge challenge;
+        TlvList   requestedTlvList;
 
         // Challenge
         SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(challenge));
 
         // TLV Request
-        switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvs))
+        switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
         {
         case kErrorNone:
-            break;
         case kErrorNotFound:
-            requestedTlvs.mNumTlvs = 0;
             break;
         default:
             ExitNow(error = kErrorParse);
         }
 
-        SuccessOrExit(error = SendLinkAccept(aRxInfo.mMessageInfo, router, requestedTlvs, challenge));
+        SuccessOrExit(error = SendLinkAccept(aRxInfo.mMessageInfo, router, requestedTlvList, challenge));
     }
 
 exit:
     return error;
 }
 
-uint8_t MleRouter::LinkQualityToCost(uint8_t aLinkQuality)
-{
-    uint8_t rval;
-
-    switch (aLinkQuality)
-    {
-    case 1:
-        rval = kLinkQuality1LinkCost;
-        break;
-
-    case 2:
-        rval = kLinkQuality2LinkCost;
-        break;
-
-    case 3:
-        rval = kLinkQuality3LinkCost;
-        break;
-
-    default:
-        rval = kLinkQuality0LinkCost;
-        break;
-    }
-
-    return rval;
-}
-
-uint8_t MleRouter::GetLinkCost(uint8_t aRouterId)
-{
-    uint8_t rval = kMaxRouteCost;
-    Router *router;
-
-    router = mRouterTable.GetRouter(aRouterId);
-
-    // `nullptr` aRouterId indicates non-existing next hop, hence return kMaxRouteCost for it.
-    VerifyOrExit(router != nullptr);
-
-    rval = mRouterTable.GetLinkCost(*router);
-
-exit:
-    return rval;
-}
-
 Error MleRouter::SetRouterSelectionJitter(uint8_t aRouterJitter)
 {
     Error error = kErrorNone;
@@ -1111,20 +1028,9 @@
     return error;
 }
 
-Error MleRouter::ProcessRouteTlv(RxInfo &aRxInfo)
+Error MleRouter::ProcessRouteTlv(const RouteTlv &aRouteTlv, RxInfo &aRxInfo)
 {
-    RouteTlv routeTlv;
-
-    return ProcessRouteTlv(aRxInfo, routeTlv);
-}
-
-Error MleRouter::ProcessRouteTlv(RxInfo &aRxInfo, RouteTlv &aRouteTlv)
-{
-    // This method processes Route TLV in a received MLE message
-    // (from `RxInfo`). In case of success, `aRouteTlv` is updated
-    // to return the read/processed route TLV from the message.
-    // If the message contains no Route TLV, `kErrorNotFound` is
-    // returned.
+    // This method processes `aRouteTlv` read from an MLE message.
     //
     // During processing of Route TLV, the entries in the router table
     // may shuffle. This method ensures that the `aRxInfo.mNeighbor`
@@ -1133,7 +1039,7 @@
     // (in case `mNeighbor` was pointing to a router entry from the
     // `RouterTable`).
 
-    Error    error;
+    Error    error          = kErrorNone;
     uint16_t neighborRloc16 = Mac::kShortAddrInvalid;
 
     if ((aRxInfo.mNeighbor != nullptr) && Get<RouterTable>().Contains(*aRxInfo.mNeighbor))
@@ -1141,13 +1047,9 @@
         neighborRloc16 = aRxInfo.mNeighbor->GetRloc16();
     }
 
-    SuccessOrExit(error = Tlv::FindTlv(aRxInfo.mMessage, aRouteTlv));
+    mRouterTable.UpdateRouterIdSet(aRouteTlv.GetRouterIdSequence(), aRouteTlv.GetRouterIdMask());
 
-    VerifyOrExit(aRouteTlv.IsValid(), error = kErrorParse);
-
-    Get<RouterTable>().UpdateRouterIdSet(aRouteTlv.GetRouterIdSequence(), aRouteTlv.GetRouterIdMask());
-
-    if (IsRouter() && !Get<RouterTable>().IsAllocated(mRouterId))
+    if (IsRouter() && !mRouterTable.IsAllocated(mRouterId))
     {
         IgnoreError(BecomeDetached());
         error = kErrorNoRoute;
@@ -1155,28 +1057,53 @@
 
     if (neighborRloc16 != Mac::kShortAddrInvalid)
     {
-        aRxInfo.mNeighbor = Get<RouterTable>().GetNeighbor(neighborRloc16);
+        aRxInfo.mNeighbor = Get<NeighborTable>().FindNeighbor(neighborRloc16);
+    }
+
+    return error;
+}
+
+Error MleRouter::ReadAndProcessRouteTlvOnFed(RxInfo &aRxInfo, uint8_t aParentId)
+{
+    // This method reads and processes Route TLV from message on an
+    // FED if message contains one. It returns `kErrorNone` when
+    // successfully processed or if there is no Route TLV in the
+    // message.
+    //
+    // It MUST be used only when device is acting as a child and
+    // for a message received from device's current parent.
+
+    Error    error = kErrorNone;
+    RouteTlv routeTlv;
+
+    VerifyOrExit(IsFullThreadDevice());
+
+    switch (aRxInfo.mMessage.ReadRouteTlv(routeTlv))
+    {
+    case kErrorNone:
+        SuccessOrExit(error = ProcessRouteTlv(routeTlv, aRxInfo));
+        mRouterTable.UpdateRoutesOnFed(routeTlv, aParentId);
+        mRequestRouteTlv = false;
+        break;
+    case kErrorNotFound:
+        break;
+    default:
+        ExitNow(error = kErrorParse);
     }
 
 exit:
     return error;
 }
 
-bool MleRouter::IsSingleton(void)
+bool MleRouter::IsSingleton(void) const
 {
-    bool rval = true;
+    bool isSingleton = true;
 
-    if (IsAttached() && IsRouterEligible())
-    {
-        // not a singleton if any other routers exist
-        if (mRouterTable.GetActiveRouterCount() > 1)
-        {
-            ExitNow(rval = false);
-        }
-    }
+    VerifyOrExit(IsAttached() && IsRouterEligible());
+    isSingleton = (mRouterTable.GetActiveRouterCount() <= 1);
 
 exit:
-    return rval;
+    return isSingleton;
 }
 
 int MleRouter::ComparePartitions(bool              aSingletonA,
@@ -1186,103 +1113,69 @@
 {
     int rval = 0;
 
-    if (aLeaderDataA.GetWeighting() != aLeaderDataB.GetWeighting())
-    {
-        ExitNow(rval = aLeaderDataA.GetWeighting() > aLeaderDataB.GetWeighting() ? 1 : -1);
-    }
+    rval = ThreeWayCompare(aLeaderDataA.GetWeighting(), aLeaderDataB.GetWeighting());
+    VerifyOrExit(rval == 0);
 
-    if (aSingletonA != aSingletonB)
-    {
-        ExitNow(rval = aSingletonB ? 1 : -1);
-    }
+    // Not being a singleton is better.
+    rval = ThreeWayCompare(!aSingletonA, !aSingletonB);
+    VerifyOrExit(rval == 0);
 
-    if (aLeaderDataA.GetPartitionId() != aLeaderDataB.GetPartitionId())
-    {
-        ExitNow(rval = aLeaderDataA.GetPartitionId() > aLeaderDataB.GetPartitionId() ? 1 : -1);
-    }
+    rval = ThreeWayCompare(aLeaderDataA.GetPartitionId(), aLeaderDataB.GetPartitionId());
 
 exit:
     return rval;
 }
 
-bool MleRouter::IsSingleton(const RouteTlv &aRouteTlv)
+Error MleRouter::HandleAdvertisement(RxInfo &aRxInfo, uint16_t aSourceAddress, const LeaderData &aLeaderData)
 {
-    bool    rval  = true;
-    uint8_t count = 0;
+    // This method processes a received MLE Advertisement message on
+    // an FTD device. It is called from `Mle::HandleAdvertisement()`
+    // only when device is attached (in child, router, or leader roles)
+    // and `IsFullThreadDevice()`.
+    //
+    // - `aSourceAddress` is the read value from `SourceAddressTlv`.
+    // - `aLeaderData` is the read value from `LeaderDataTlv`.
 
-    // REEDs do not include a Route TLV and indicate not a singleton
-    if (!aRouteTlv.IsValid())
+    Error    error      = kErrorNone;
+    uint8_t  linkMargin = Get<Mac::Mac>().ComputeLinkMargin(aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss());
+    RouteTlv routeTlv;
+    Router  *router;
+    uint8_t  routerId;
+
+    switch (aRxInfo.mMessage.ReadRouteTlv(routeTlv))
     {
-        ExitNow(rval = false);
+    case kErrorNone:
+        break;
+    case kErrorNotFound:
+        routeTlv.SetLength(0); // Mark that a Route TLV was not included.
+        break;
+    default:
+        ExitNow(error = kErrorParse);
     }
 
-    // Check if 2 or more active routers
-    for (uint8_t routerId = 0; routerId <= kMaxRouterId; routerId++)
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Handle Partition ID mismatch
+
+    if (aLeaderData.GetPartitionId() != mLeaderData.GetPartitionId())
     {
-        if (aRouteTlv.IsRouterIdSet(routerId) && (++count >= 2))
+        LogNote("Different partition (peer:%lu, local:%lu)", ToUlong(aLeaderData.GetPartitionId()),
+                ToUlong(mLeaderData.GetPartitionId()));
+
+        VerifyOrExit(linkMargin >= kPartitionMergeMinMargin, error = kErrorLinkMarginLow);
+
+        if (routeTlv.IsValid() && (mPreviousPartitionIdTimeout > 0) &&
+            (aLeaderData.GetPartitionId() == mPreviousPartitionId))
         {
-            ExitNow(rval = false);
-        }
-    }
-
-exit:
-    return rval;
-}
-
-Error MleRouter::HandleAdvertisement(RxInfo &aRxInfo)
-{
-    Error                 error    = kErrorNone;
-    const ThreadLinkInfo *linkInfo = aRxInfo.mMessageInfo.GetThreadLinkInfo();
-    uint8_t linkMargin = LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(), linkInfo->GetRss());
-    Mac::ExtAddress extAddr;
-    uint16_t        sourceAddress = Mac::kShortAddrInvalid;
-    LeaderData      leaderData;
-    RouteTlv        route;
-    uint32_t        partitionId;
-    Router *        router;
-    uint8_t         routerId;
-    uint8_t         routerCount;
-
-    aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(extAddr);
-
-    // Source Address
-    SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));
-
-    // Leader Data
-    SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
-
-    // Route Data (optional)
-    if (Tlv::FindTlv(aRxInfo.mMessage, route) == kErrorNone)
-    {
-        VerifyOrExit(route.IsValid(), error = kErrorParse);
-    }
-    else
-    {
-        // mark that a Route TLV was not included
-        route.SetLength(0);
-    }
-
-    partitionId = leaderData.GetPartitionId();
-
-    if (partitionId != mLeaderData.GetPartitionId())
-    {
-        LogNote("Different partition (peer:%u, local:%u)", partitionId, mLeaderData.GetPartitionId());
-
-        VerifyOrExit(linkMargin >= OPENTHREAD_CONFIG_MLE_PARTITION_MERGE_MARGIN_MIN, error = kErrorLinkMarginLow);
-
-        if (route.IsValid() && IsFullThreadDevice() && (mPreviousPartitionIdTimeout > 0) &&
-            (partitionId == mPreviousPartitionId))
-        {
-            VerifyOrExit(SerialNumber::IsGreater(route.GetRouterIdSequence(), mPreviousPartitionRouterIdSequence),
+            VerifyOrExit(SerialNumber::IsGreater(routeTlv.GetRouterIdSequence(), mPreviousPartitionRouterIdSequence),
                          error = kErrorDrop);
         }
 
-        if (IsChild() && (aRxInfo.mNeighbor == &mParent || !IsFullThreadDevice()))
+        if (IsChild() && (aRxInfo.mNeighbor == &mParent))
         {
             ExitNow();
         }
 
-        if (ComparePartitions(IsSingleton(route), leaderData, IsSingleton(), mLeaderData) > 0
+        if (ComparePartitions(routeTlv.IsSingleton(), aLeaderData, IsSingleton(), mLeaderData) > 0
 #if OPENTHREAD_CONFIG_TIME_SYNC_REQUIRED
             // if time sync is required, it will only migrate to a better network which also enables time sync.
             && aRxInfo.mMessage.GetTimeSyncSeq() != OT_TIME_SYNC_INVALID_SEQ
@@ -1294,9 +1187,13 @@
 
         ExitNow(error = kErrorDrop);
     }
-    else if (leaderData.GetLeaderRouterId() != GetLeaderId())
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Handle Leader Router ID mismatch
+
+    if (aLeaderData.GetLeaderRouterId() != GetLeaderId())
     {
-        VerifyOrExit(aRxInfo.mNeighbor && aRxInfo.mNeighbor->IsStateValid());
+        VerifyOrExit(aRxInfo.IsNeighborStateValid());
 
         if (!IsChild())
         {
@@ -1308,130 +1205,80 @@
         ExitNow();
     }
 
-    VerifyOrExit(IsActiveRouter(sourceAddress) && route.IsValid());
-    routerId = RouterIdFromRloc16(sourceAddress);
+    VerifyOrExit(IsActiveRouter(aSourceAddress) && routeTlv.IsValid());
+    routerId = RouterIdFromRloc16(aSourceAddress);
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
     Get<TimeSync>().HandleTimeSyncMessage(aRxInfo.mMessage);
 #endif
 
-    if (IsFullThreadDevice() && (aRxInfo.mNeighbor && aRxInfo.mNeighbor->IsStateValid()) &&
-        ((mRouterTable.GetActiveRouterCount() == 0) ||
-         SerialNumber::IsGreater(route.GetRouterIdSequence(), mRouterTable.GetRouterIdSequence())))
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Process `RouteTlv`
+
+    if (aRxInfo.IsNeighborStateValid() && mRouterTable.IsRouteTlvIdSequenceMoreRecent(routeTlv))
     {
         bool processRouteTlv = false;
 
-        switch (mRole)
+        if (IsChild())
         {
-        case kRoleDisabled:
-        case kRoleDetached:
-            break;
-
-        case kRoleChild:
-            if (sourceAddress == mParent.GetRloc16())
+            if (aSourceAddress == mParent.GetRloc16())
             {
                 processRouteTlv = true;
             }
             else
             {
-                router = mRouterTable.GetRouter(routerId);
+                router = mRouterTable.FindRouterById(routerId);
 
                 if (router != nullptr && router->IsStateValid())
                 {
                     processRouteTlv = true;
                 }
             }
-
-            break;
-
-        case kRoleRouter:
-        case kRoleLeader:
+        }
+        else // Device is router or leader
+        {
             processRouteTlv = true;
-            break;
         }
 
         if (processRouteTlv)
         {
-            SuccessOrExit(error = ProcessRouteTlv(aRxInfo));
+            SuccessOrExit(error = ProcessRouteTlv(routeTlv, aRxInfo));
         }
     }
 
-    switch (mRole)
-    {
-    case kRoleDisabled:
-    case kRoleDetached:
-        ExitNow();
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Update routers as a child
 
-    case kRoleChild:
+    if (IsChild())
+    {
         if (aRxInfo.mNeighbor == &mParent)
         {
             // MLE Advertisement from parent
             router = &mParent;
 
-            if (mParent.GetRloc16() != sourceAddress)
+            if (mParent.GetRloc16() != aSourceAddress)
             {
                 IgnoreError(BecomeDetached());
                 ExitNow(error = kErrorDetached);
             }
 
-            if (IsFullThreadDevice())
+            if ((mRouterSelectionJitterTimeout == 0) && (mRouterTable.GetActiveRouterCount() < mRouterUpgradeThreshold))
             {
-                Router *leader;
-
-                if ((mRouterSelectionJitterTimeout == 0) &&
-                    (mRouterTable.GetActiveRouterCount() < mRouterUpgradeThreshold))
-                {
-                    mRouterSelectionJitterTimeout = 1 + Random::NonCrypto::GetUint8InRange(0, mRouterSelectionJitter);
-                    ExitNow();
-                }
-
-                leader = mRouterTable.GetLeader();
-
-                if (leader != nullptr)
-                {
-                    for (uint8_t id = 0, routeCount = 0; id <= kMaxRouterId; id++)
-                    {
-                        if (!route.IsRouterIdSet(id))
-                        {
-                            continue;
-                        }
-
-                        if (id != GetLeaderId())
-                        {
-                            routeCount++;
-                            continue;
-                        }
-
-                        if (route.GetRouteCost(routeCount) > 0)
-                        {
-                            leader->SetNextHop(id);
-                            leader->SetCost(route.GetRouteCost(routeCount));
-                        }
-                        else
-                        {
-                            leader->SetNextHop(kInvalidRouterId);
-                            leader->SetCost(0);
-                        }
-
-                        break;
-                    }
-                }
+                mRouterSelectionJitterTimeout = 1 + Random::NonCrypto::GetUint8InRange(0, mRouterSelectionJitter);
             }
+
+            mRouterTable.UpdateRoutesOnFed(routeTlv, routerId);
         }
         else
         {
             // MLE Advertisement not from parent, but from some other neighboring router
-            router = mRouterTable.GetRouter(routerId);
+            router = mRouterTable.FindRouterById(routerId);
             VerifyOrExit(router != nullptr);
 
-            if (IsFullThreadDevice() && !router->IsStateValid() && !router->IsStateLinkRequest() &&
-                (mRouterTable.GetActiveLinkCount() < OPENTHREAD_CONFIG_MLE_CHILD_ROUTER_LINKS))
+            if (!router->IsStateValid() && !router->IsStateLinkRequest() &&
+                (mRouterTable.GetNeighborCount() < mChildRouterLinks))
             {
-                router->SetExtAddress(extAddr);
-                router->GetLinkInfo().Clear();
-                router->GetLinkInfo().AddRss(linkInfo->GetRss());
-                router->ResetLinkFailures();
-                router->SetLastHeard(TimerMilli::GetNow());
+                InitNeighbor(*router, aRxInfo);
                 router->SetState(Neighbor::kStateLinkRequest);
                 IgnoreError(SendLinkRequest(router));
                 ExitNow(error = kErrorNoRoute);
@@ -1441,67 +1288,55 @@
         router->SetLastHeard(TimerMilli::GetNow());
 
         ExitNow();
+    }
 
-    case kRoleRouter:
-        if (mLinkRequestDelay > 0 && route.IsRouterIdSet(mRouterId))
-        {
-            mLinkRequestDelay = 0;
-            IgnoreError(SendLinkRequest(nullptr));
-        }
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Update routers as a router or leader.
 
-        router = mRouterTable.GetRouter(routerId);
-        VerifyOrExit(router != nullptr);
-
-        // check current active router number
-        routerCount = 0;
-
-        for (uint8_t id = 0; id <= kMaxRouterId; id++)
-        {
-            if (route.IsRouterIdSet(id))
-            {
-                routerCount++;
-            }
-        }
-
-        if (routerCount > mRouterDowngradeThreshold && mRouterSelectionJitterTimeout == 0 &&
-            HasMinDowngradeNeighborRouters() && HasSmallNumberOfChildren() &&
-            HasOneNeighborWithComparableConnectivity(route, routerId)
-#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTER_REQUEST_ROUTER_ROLE
-            && !Get<NetworkData::Notifier>().IsEligibleForRouterRoleUpgradeAsBorderRouter()
-#endif
-        )
+    if (IsRouter())
+    {
+        if (ShouldDowngrade(routerId, routeTlv))
         {
             mRouterSelectionJitterTimeout = 1 + Random::NonCrypto::GetUint8InRange(0, mRouterSelectionJitter);
         }
-
-        OT_FALL_THROUGH;
-
-    case kRoleLeader:
-        router = mRouterTable.GetRouter(routerId);
-        VerifyOrExit(router != nullptr);
-
-        // Send unicast link request if no link to router and no unicast/multicast link request in progress
-        if (!router->IsStateValid() && !router->IsStateLinkRequest() && (mChallengeTimeout == 0) &&
-            (linkMargin >= OPENTHREAD_CONFIG_MLE_LINK_REQUEST_MARGIN_MIN))
-        {
-            router->SetExtAddress(extAddr);
-            router->GetLinkInfo().Clear();
-            router->GetLinkInfo().AddRss(linkInfo->GetRss());
-            router->ResetLinkFailures();
-            router->SetLastHeard(TimerMilli::GetNow());
-            router->SetState(Neighbor::kStateLinkRequest);
-            IgnoreError(SendLinkRequest(router));
-            ExitNow(error = kErrorNoRoute);
-        }
-
-        router->SetLastHeard(TimerMilli::GetNow());
-        break;
     }
 
-    UpdateRoutes(route, routerId);
+    router = mRouterTable.FindRouterById(routerId);
+    VerifyOrExit(router != nullptr);
+
+    if (!router->IsStateValid() && aRxInfo.IsNeighborStateValid() && Get<ChildTable>().Contains(*aRxInfo.mNeighbor))
+    {
+        // The Adv is from a former child that is now acting as a router,
+        // we copy the info from child entry and update the RLOC16.
+
+        *static_cast<Neighbor *>(router) = *aRxInfo.mNeighbor;
+        router->SetRloc16(Rloc16FromRouterId(routerId));
+        router->SetDeviceMode(DeviceMode(DeviceMode::kModeFullThreadDevice | DeviceMode::kModeRxOnWhenIdle |
+                                         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
+    if (!router->IsStateValid() && !router->IsStateLinkRequest() && (mChallengeTimeout == 0) &&
+        (linkMargin >= kLinkRequestMinMargin))
+    {
+        InitNeighbor(*router, aRxInfo);
+        router->SetState(Neighbor::kStateLinkRequest);
+        IgnoreError(SendLinkRequest(router));
+        ExitNow(error = kErrorNoRoute);
+    }
+
+    router->SetLastHeard(TimerMilli::GetNow());
+
+    mRouterTable.UpdateRoutes(routeTlv, routerId);
 
 exit:
-    if (aRxInfo.mNeighbor && aRxInfo.mNeighbor->GetRloc16() != sourceAddress)
+    if (aRxInfo.mNeighbor && aRxInfo.mNeighbor->GetRloc16() != aSourceAddress)
     {
         // Remove stale neighbors
         RemoveNeighbor(*aRxInfo.mNeighbor);
@@ -1510,165 +1345,6 @@
     return error;
 }
 
-void MleRouter::UpdateRoutes(const RouteTlv &aRoute, uint8_t aRouterId)
-{
-    Router *neighbor;
-    bool    resetAdvInterval = false;
-    bool    changed          = false;
-
-    neighbor = mRouterTable.GetRouter(aRouterId);
-    VerifyOrExit(neighbor != nullptr);
-
-    // update link quality out to neighbor
-    changed = UpdateLinkQualityOut(aRoute, *neighbor, resetAdvInterval);
-
-    // update routes
-    for (uint8_t routerId = 0, routeCount = 0; routerId <= kMaxRouterId; routerId++)
-    {
-        Router *router;
-        Router *nextHop;
-        uint8_t oldNextHop;
-        uint8_t cost;
-
-        if (!aRoute.IsRouterIdSet(routerId))
-        {
-            continue;
-        }
-
-        router = mRouterTable.GetRouter(routerId);
-
-        if (router == nullptr || router->GetRloc16() == GetRloc16() || router == neighbor)
-        {
-            routeCount++;
-            continue;
-        }
-
-        oldNextHop = router->GetNextHop();
-        nextHop    = mRouterTable.GetRouter(oldNextHop);
-
-        cost = aRoute.GetRouteCost(routeCount);
-
-        if (cost == 0)
-        {
-            cost = kMaxRouteCost;
-        }
-
-        if (nextHop == nullptr || nextHop == neighbor)
-        {
-            // router has no next hop or next hop is neighbor (sender)
-
-            if (cost + mRouterTable.GetLinkCost(*neighbor) < kMaxRouteCost)
-            {
-                if (nextHop == nullptr && mRouterTable.GetLinkCost(*router) >= kMaxRouteCost)
-                {
-                    resetAdvInterval = true;
-                }
-
-                if (router->GetNextHop() != aRouterId)
-                {
-                    router->SetNextHop(aRouterId);
-                    changed = true;
-                }
-
-                if (router->GetCost() != cost)
-                {
-                    router->SetCost(cost);
-                    changed = true;
-                }
-            }
-            else if (nextHop == neighbor)
-            {
-                if (mRouterTable.GetLinkCost(*router) >= kMaxRouteCost)
-                {
-                    resetAdvInterval = true;
-                }
-
-                router->SetNextHop(kInvalidRouterId);
-                router->SetCost(0);
-                router->SetLastHeard(TimerMilli::GetNow());
-                changed = true;
-            }
-        }
-        else
-        {
-            uint8_t curCost = router->GetCost() + mRouterTable.GetLinkCost(*nextHop);
-            uint8_t newCost = cost + mRouterTable.GetLinkCost(*neighbor);
-
-            if (newCost < curCost)
-            {
-                router->SetNextHop(aRouterId);
-                router->SetCost(cost);
-                changed = true;
-            }
-        }
-
-        routeCount++;
-    }
-
-    if (resetAdvInterval)
-    {
-        ResetAdvertiseInterval();
-    }
-
-#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
-
-    VerifyOrExit(changed);
-    LogInfo("Route table updated");
-
-    for (Router &router : Get<RouterTable>().Iterate())
-    {
-        LogInfo("    %04x -> %04x, cost:%d %d, lqin:%d, lqout:%d, link:%s", router.GetRloc16(),
-                (router.GetNextHop() == kInvalidRouterId) ? 0xffff : Rloc16FromRouterId(router.GetNextHop()),
-                router.GetCost(), mRouterTable.GetLinkCost(router), router.GetLinkInfo().GetLinkQuality(),
-                router.GetLinkQualityOut(),
-                router.GetRloc16() == GetRloc16() ? "device" : ToYesNo(router.IsStateValid()));
-    }
-
-#else
-    OT_UNUSED_VARIABLE(changed);
-#endif
-
-exit:
-    return;
-}
-
-bool MleRouter::UpdateLinkQualityOut(const RouteTlv &aRoute, Router &aNeighbor, bool &aResetAdvInterval)
-{
-    bool        changed = false;
-    LinkQuality linkQuality;
-    uint8_t     myRouterId;
-    uint8_t     myRouteCount;
-    uint8_t     oldLinkCost;
-    Router *    nextHop;
-
-    myRouterId = RouterIdFromRloc16(GetRloc16());
-    VerifyOrExit(aRoute.IsRouterIdSet(myRouterId));
-
-    myRouteCount = 0;
-    for (uint8_t routerId = 0; routerId < myRouterId; routerId++)
-    {
-        myRouteCount += aRoute.IsRouterIdSet(routerId);
-    }
-
-    linkQuality = aRoute.GetLinkQualityIn(myRouteCount);
-    VerifyOrExit(aNeighbor.GetLinkQualityOut() != linkQuality);
-
-    oldLinkCost = mRouterTable.GetLinkCost(aNeighbor);
-
-    aNeighbor.SetLinkQualityOut(linkQuality);
-    nextHop = mRouterTable.GetRouter(aNeighbor.GetNextHop());
-
-    // reset MLE advertisement timer if neighbor route cost changed to or from infinite
-    if (nextHop == nullptr && (oldLinkCost >= kMaxRouteCost) != (mRouterTable.GetLinkCost(aNeighbor) >= kMaxRouteCost))
-    {
-        aResetAdvInterval = true;
-    }
-    changed = true;
-
-exit:
-    return changed;
-}
-
 void MleRouter::HandleParentRequest(RxInfo &aRxInfo)
 {
     Error           error = kErrorNone;
@@ -1676,8 +1352,7 @@
     uint16_t        version;
     uint8_t         scanMask;
     Challenge       challenge;
-    Router *        leader;
-    Child *         child;
+    Child          *child;
     uint8_t         modeBitmask;
     DeviceMode      mode;
 
@@ -1700,13 +1375,7 @@
     VerifyOrExit(mRouterTable.GetLeaderAge() < mNetworkIdTimeout, error = kErrorDrop);
 
     // 3. Its current routing path cost to the Leader is infinite.
-    leader = mRouterTable.GetLeader();
-    OT_ASSERT(leader != nullptr);
-
-    VerifyOrExit(IsLeader() || GetLinkCost(GetLeaderId()) < kMaxRouteCost ||
-                     (IsChild() && leader->GetCost() + 1 < kMaxRouteCost) ||
-                     (leader->GetCost() + GetLinkCost(leader->GetNextHop()) < kMaxRouteCost),
-                 error = kErrorDrop);
+    VerifyOrExit(mRouterTable.GetPathCostToLeader() < kMaxRouteCost, error = kErrorDrop);
 
     // 4. It is a REED and there are already `kMaxRouters` active routers in
     // the network (because Leader would reject any further address solicit).
@@ -1716,7 +1385,7 @@
 
     // Version
     SuccessOrExit(error = Tlv::Find<VersionTlv>(aRxInfo.mMessage, version));
-    VerifyOrExit(version >= OT_THREAD_VERSION_1_1, error = kErrorParse);
+    VerifyOrExit(version >= kThreadVersion1p1, error = kErrorParse);
 
     // Scan Mask
     SuccessOrExit(error = Tlv::Find<ScanMaskTlv>(aRxInfo.mMessage, scanMask));
@@ -1748,10 +1417,7 @@
         VerifyOrExit((child = mChildTable.GetNewChild()) != nullptr, error = kErrorNoBufs);
 
         // MAC Address
-        child->SetExtAddress(extAddr);
-        child->GetLinkInfo().Clear();
-        child->GetLinkInfo().AddRss(aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss());
-        child->ResetLinkFailures();
+        InitNeighbor(*child, aRxInfo);
         child->SetState(Neighbor::kStateParentRequest);
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
         child->SetTimeSyncEnabled(Tlv::Find<TimeRequestTlv>(aRxInfo.mMessage, nullptr, 0) == kErrorNone);
@@ -1760,7 +1426,7 @@
         {
             mode.Set(modeBitmask);
             child->SetDeviceMode(mode);
-            child->SetVersion(static_cast<uint8_t>(version));
+            child->SetVersion(version);
         }
     }
     else if (TimerMilli::GetNow() - child->GetLastHeard() < kParentRequestRouterTimeout - kParentRequestDuplicateMargin)
@@ -1771,7 +1437,7 @@
     if (!child->IsStateValidOrRestoring())
     {
         child->SetLastHeard(TimerMilli::GetNow());
-        child->SetTimeout(Time::MsecToSec(kMaxChildIdRequestTimeout));
+        child->SetTimeout(Time::MsecToSec(kChildIdRequestTimeout));
     }
 
     aRxInfo.mClass = RxInfo::kPeerMessage;
@@ -1787,25 +1453,23 @@
     bool    haveNeighbor = true;
     uint8_t linkMargin;
 
-    linkMargin =
-        LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(), mParent.GetLinkInfo().GetLastRss());
+    linkMargin = Get<Mac::Mac>().ComputeLinkMargin(mParent.GetLinkInfo().GetLastRss());
 
-    if (linkMargin >= OPENTHREAD_CONFIG_MLE_LINK_REQUEST_MARGIN_MIN)
+    if (linkMargin >= kLinkRequestMinMargin)
     {
         ExitNow();
     }
 
-    for (Router &router : Get<RouterTable>().Iterate())
+    for (const Router &router : Get<RouterTable>())
     {
         if (!router.IsStateValid())
         {
             continue;
         }
 
-        linkMargin =
-            LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(), router.GetLinkInfo().GetLastRss());
+        linkMargin = Get<Mac::Mac>().ComputeLinkMargin(router.GetLinkInfo().GetLastRss());
 
-        if (linkMargin >= OPENTHREAD_CONFIG_MLE_LINK_REQUEST_MARGIN_MIN)
+        if (linkMargin >= kLinkRequestMinMargin)
         {
             ExitNow();
         }
@@ -1823,11 +1487,6 @@
 
     VerifyOrExit(IsFullThreadDevice(), Get<TimeTicker>().UnregisterReceiver(TimeTicker::kMleRouter));
 
-    if (mLinkRequestDelay > 0 && --mLinkRequestDelay == 0)
-    {
-        IgnoreError(SendLinkRequest(nullptr));
-    }
-
     if (mChallengeTimeout > 0)
     {
         mChallengeTimeout--;
@@ -1870,12 +1529,8 @@
 
     switch (mRole)
     {
-    case kRoleDisabled:
-        OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
-
     case kRoleDetached:
-        if (mChallengeTimeout == 0)
+        if (mChallengeTimeout == 0 && mLinkRequestAttempts == 0)
         {
             IgnoreError(BecomeDetached());
             ExitNow();
@@ -1912,7 +1567,7 @@
 
     case kRoleRouter:
         // verify path to leader
-        LogDebg("network id timeout = %d", mRouterTable.GetLeaderAge());
+        LogDebg("network id timeout = %lu", ToUlong(mRouterTable.GetLeaderAge()));
 
         if ((mRouterTable.GetActiveRouterCount() > 0) && (mRouterTable.GetLeaderAge() >= mNetworkIdTimeout))
         {
@@ -1930,6 +1585,9 @@
 
     case kRoleLeader:
         break;
+
+    case kRoleDisabled:
+        OT_ASSERT(false);
     }
 
     // update children state
@@ -1953,7 +1611,6 @@
         case Neighbor::kStateParentResponse:
         case Neighbor::kStateLinkRequest:
             OT_ASSERT(false);
-            OT_UNREACHABLE_CODE(break);
         }
 
 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
@@ -1978,7 +1635,7 @@
     }
 
     // update router state
-    for (Router &router : Get<RouterTable>().Iterate())
+    for (Router &router : Get<RouterTable>())
     {
         uint32_t age;
 
@@ -2021,7 +1678,7 @@
         }
         else if (router.IsStateLinkRequest())
         {
-            if (age >= kMaxLinkRequestTimeout)
+            if (age >= kLinkRequestTimeout)
             {
                 LogInfo("Link Request timeout expired");
                 RemoveNeighbor(router);
@@ -2031,8 +1688,8 @@
 
         if (IsLeader())
         {
-            if (mRouterTable.GetRouter(router.GetNextHop()) == nullptr &&
-                mRouterTable.GetLinkCost(router) >= kMaxRouteCost && age >= Time::SecToMsec(kMaxLeaderToRouterTimeout))
+            if (mRouterTable.FindNextHopOf(router) == nullptr && mRouterTable.GetLinkCost(router) >= kMaxRouteCost &&
+                age >= Time::SecToMsec(kMaxLeaderToRouterTimeout))
             {
                 LogInfo("Router ID timeout expired (no route)");
                 IgnoreError(mRouterTable.Release(router.GetRouterId()));
@@ -2059,7 +1716,7 @@
 {
     Error        error = kErrorNone;
     Ip6::Address destination;
-    TxMessage *  message;
+    TxMessage   *message;
     uint16_t     delay;
 
     VerifyOrExit((message = NewMleMessage(kCommandParentResponse)) != nullptr, error = kErrorNoBufs);
@@ -2084,24 +1741,15 @@
 #endif
 
     aChild->GenerateChallenge();
-
     SuccessOrExit(error = message->AppendChallengeTlv(aChild->GetChallenge(), aChild->GetChallengeSize()));
-    error = message->AppendLinkMarginTlv(aChild->GetLinkInfo().GetLinkMargin());
-    SuccessOrExit(error);
-
+    SuccessOrExit(error = message->AppendLinkMarginTlv(aChild->GetLinkInfo().GetLinkMargin()));
     SuccessOrExit(error = message->AppendConnectivityTlv());
     SuccessOrExit(error = message->AppendVersionTlv());
 
     destination.SetToLinkLocalAddress(aChild->GetExtAddress());
 
-    if (aRoutersOnlyRequest)
-    {
-        delay = 1 + Random::NonCrypto::GetUint16InRange(0, kParentResponseMaxDelayRouters);
-    }
-    else
-    {
-        delay = 1 + Random::NonCrypto::GetUint16InRange(0, kParentResponseMaxDelayAll);
-    }
+    delay = 1 + Random::NonCrypto::GetUint16InRange(0, aRoutersOnlyRequest ? kParentResponseMaxDelayRouters
+                                                                           : kParentResponseMaxDelayAll);
 
     SuccessOrExit(error = message->SendAfterDelay(destination, delay));
 
@@ -2114,7 +1762,7 @@
 
 uint8_t MleRouter::GetMaxChildIpAddresses(void) const
 {
-    uint8_t num = OPENTHREAD_CONFIG_MLE_IP_ADDRS_PER_CHILD;
+    uint8_t num = kMaxChildIpAddresses;
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     if (mMaxChildIpAddresses != 0)
@@ -2131,7 +1779,7 @@
 {
     Error error = kErrorNone;
 
-    VerifyOrExit(aMaxIpAddresses <= OPENTHREAD_CONFIG_MLE_IP_ADDRS_PER_CHILD, error = kErrorInvalidArgs);
+    VerifyOrExit(aMaxIpAddresses <= kMaxChildIpAddresses, error = kErrorInvalidArgs);
 
     mMaxChildIpAddresses = aMaxIpAddresses;
 
@@ -2140,42 +1788,36 @@
 }
 #endif
 
-Error MleRouter::UpdateChildAddresses(const Message &aMessage, uint16_t aOffset, Child &aChild)
+Error MleRouter::ProcessAddressRegistrationTlv(RxInfo &aRxInfo, Child &aChild)
 {
-    Error                    error = kErrorNone;
-    AddressRegistrationEntry entry;
-    Ip6::Address             address;
-    Lowpan::Context          context;
-    Tlv                      tlv;
-    uint8_t                  registeredCount = 0;
-    uint8_t                  storedCount     = 0;
-    uint16_t                 offset          = 0;
-    uint16_t                 end             = 0;
-#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
+    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_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
-    Ip6::Address oldMlrRegisteredAddresses[OPENTHREAD_CONFIG_MLE_IP_ADDRS_PER_CHILD - 1];
+#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
+    Ip6::Address oldMlrRegisteredAddresses[kMaxChildIpAddresses - 1];
     uint16_t     oldMlrRegisteredAddressNum = 0;
 #endif
 
-    SuccessOrExit(error = aMessage.Read(aOffset, tlv));
-    VerifyOrExit(tlv.GetLength() <= (aMessage.GetLength() - aOffset - sizeof(tlv)), error = kErrorParse);
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kAddressRegistration, offset, length));
 
-    offset = aOffset + sizeof(tlv);
-    end    = offset + tlv.GetLength();
+    endOffset = offset + length;
 
-#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
+#if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
     if ((oldDuaPtr = aChild.GetDomainUnicastAddress()) != nullptr)
     {
         oldDua = *oldDuaPtr;
     }
 #endif
 
-#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
+#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
     // Retrieve registered multicast addresses of the Child
     if (aChild.HasAnyMlrRegisteredAddress())
     {
@@ -2194,36 +1836,47 @@
 
     aChild.ClearIp6Addresses();
 
-    while (offset < end)
+    while (offset < endOffset)
     {
-        uint8_t len;
+        uint8_t      controlByte;
+        Ip6::Address address;
 
-        // read out the control field
-        SuccessOrExit(error = aMessage.Read(offset, &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(offset, &entry, len));
-
-        offset += 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 %d 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
@@ -2245,7 +1898,7 @@
         {
             storedCount++;
 
-#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
+#if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
             if (Get<BackboneRouter::Leader>().IsDomainUnicast(address))
             {
                 hasDua = true;
@@ -2262,12 +1915,12 @@
             }
 #endif
 
-            LogInfo("Child 0x%04x IPv6 address[%d]=%s", aChild.GetRloc16(), storedCount,
+            LogInfo("Child 0x%04x IPv6 address[%u]=%s", aChild.GetRloc16(), storedCount,
                     address.ToString().AsCString());
         }
         else
         {
-#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
+#if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
             if (Get<BackboneRouter::Leader>().IsDomainUnicast(address))
             {
                 // if not able to store DUA, then assume child does not have one
@@ -2305,9 +1958,9 @@
         }
 
         // Clear EID-to-RLOC cache for the unicast address registered by the child.
-        Get<AddressResolver>().Remove(address);
+        Get<AddressResolver>().RemoveEntryForAddress(address);
     }
-#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
+#if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
     // Dua is removed
     if (oldDuaPtr != nullptr && !hasDua)
     {
@@ -2315,18 +1968,18 @@
     }
 #endif
 
-#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
+#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
     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 %d registered IPv6 address%s, %d 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;
@@ -2346,14 +1999,14 @@
     uint8_t            modeBitmask;
     DeviceMode         mode;
     uint32_t           timeout;
-    RequestedTlvs      requestedTlvs;
+    TlvList            requestedTlvList;
     MeshCoP::Timestamp timestamp;
     bool               needsActiveDatasetTlv;
     bool               needsPendingDatasetTlv;
-    Child *            child;
-    Router *           router;
+    Child             *child;
+    Router            *router;
     uint8_t            numTlvs;
-    uint16_t           addressRegistrationOffset = 0;
+    uint16_t           supervisionInterval;
 
     Log(kMessageReceive, kTypeChildIdRequest, aRxInfo.mMessageInfo.GetPeerAddr());
 
@@ -2370,7 +2023,7 @@
 
     // Version
     SuccessOrExit(error = Tlv::Find<VersionTlv>(aRxInfo.mMessage, version));
-    VerifyOrExit(version >= OT_THREAD_VERSION_1_1, error = kErrorParse);
+    VerifyOrExit(version >= kThreadVersion1p1, error = kErrorParse);
 
     // Response
     SuccessOrExit(error = aRxInfo.mMessage.ReadResponseTlv(response));
@@ -2392,9 +2045,20 @@
     // Timeout
     SuccessOrExit(error = Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout));
 
+    // Supervision interval
+    switch (Tlv::Find<SupervisionIntervalTlv>(aRxInfo.mMessage, supervisionInterval))
+    {
+    case kErrorNone:
+        break;
+    case kErrorNotFound:
+        supervisionInterval = (version <= kThreadVersion1p3) ? kChildSupervisionDefaultIntervalForOlderVersion : 0;
+        break;
+    default:
+        ExitNow(error = kErrorParse);
+    }
+
     // TLV Request
-    SuccessOrExit(error = aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvs));
-    VerifyOrExit(requestedTlvs.mNumTlvs <= Child::kMaxRequestTlvs, error = kErrorParse);
+    SuccessOrExit(error = aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList));
 
     // Active Timestamp
     needsActiveDatasetTlv = true;
@@ -2424,15 +2088,28 @@
         ExitNow(error = kErrorParse);
     }
 
+    numTlvs = requestedTlvList.GetLength();
+
+    if (needsActiveDatasetTlv)
+    {
+        numTlvs++;
+    }
+
+    if (needsPendingDatasetTlv)
+    {
+        numTlvs++;
+    }
+
+    VerifyOrExit(numTlvs <= Child::kMaxRequestTlvs, error = kErrorParse);
+
     if (!mode.IsFullThreadDevice())
     {
-        SuccessOrExit(error =
-                          Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kAddressRegistration, addressRegistrationOffset));
-        SuccessOrExit(error = UpdateChildAddresses(aRxInfo.mMessage, addressRegistrationOffset, *child));
+        SuccessOrExit(error = ProcessAddressRegistrationTlv(aRxInfo, *child));
     }
 
     // Remove from router table
-    router = mRouterTable.GetRouter(extAddr);
+    router = mRouterTable.FindRouter(extAddr);
+
     if (router != nullptr)
     {
         // The `router` here can be invalid
@@ -2454,9 +2131,10 @@
     child->SetMleFrameCounter(mleFrameCounter);
     child->SetKeySequence(aRxInfo.mKeySequence);
     child->SetDeviceMode(mode);
-    child->SetVersion(static_cast<uint8_t>(version));
+    child->SetVersion(version);
     child->GetLinkInfo().AddRss(aRxInfo.mMessageInfo.GetThreadLinkInfo()->GetRss());
     child->SetTimeout(timeout);
+    child->SetSupervisionInterval(supervisionInterval);
 #if OPENTHREAD_CONFIG_MULTI_RADIO
     child->ClearLastRxFragmentTag();
 #endif
@@ -2464,9 +2142,9 @@
     child->SetNetworkDataVersion(mLeaderData.GetDataVersion(mode.GetNetworkDataType()));
     child->ClearRequestTlvs();
 
-    for (numTlvs = 0; numTlvs < requestedTlvs.mNumTlvs; numTlvs++)
+    for (numTlvs = 0; numTlvs < requestedTlvList.GetLength(); numTlvs++)
     {
-        child->SetRequestTlv(numTlvs, requestedTlvs.mTlvs[numTlvs]);
+        child->SetRequestTlv(numTlvs, requestedTlvList[numTlvs]);
     }
 
     if (needsActiveDatasetTlv)
@@ -2483,11 +2161,6 @@
 
     switch (mRole)
     {
-    case kRoleDisabled:
-    case kRoleDetached:
-        OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
-
     case kRoleChild:
         child->SetState(Neighbor::kStateChildIdRequest);
         IgnoreError(BecomeRouter(ThreadStatusTlv::kHaveChildIdRequest));
@@ -2497,6 +2170,10 @@
     case kRoleLeader:
         SuccessOrExit(error = SendChildIdResponse(*child));
         break;
+
+    case kRoleDisabled:
+    case kRoleDetached:
+        OT_ASSERT(false);
     }
 
 exit:
@@ -2505,8 +2182,6 @@
 
 void MleRouter::HandleChildUpdateRequest(RxInfo &aRxInfo)
 {
-    static const uint8_t kMaxResponseTlvs = 10;
-
     Error           error = kErrorNone;
     Mac::ExtAddress extAddr;
     uint8_t         modeBitmask;
@@ -2514,13 +2189,12 @@
     Challenge       challenge;
     LeaderData      leaderData;
     uint32_t        timeout;
-    Child *         child;
+    uint16_t        supervisionInterval;
+    Child          *child;
     DeviceMode      oldMode;
-    RequestedTlvs   requestedTlvs;
-    uint8_t         tlvs[kMaxResponseTlvs];
-    uint8_t         tlvslength                = 0;
-    uint16_t        addressRegistrationOffset = 0;
-    bool            childDidChange            = false;
+    TlvList         requestedTlvList;
+    TlvList         tlvList;
+    bool            childDidChange = false;
 
     Log(kMessageReceive, kTypeChildUpdateRequestOfChild, aRxInfo.mMessageInfo.GetPeerAddr());
 
@@ -2532,7 +2206,7 @@
     switch (aRxInfo.mMessage.ReadChallengeTlv(challenge))
     {
     case kErrorNone:
-        tlvs[tlvslength++] = Tlv::kResponse;
+        tlvList.Add(Tlv::kResponse);
         break;
     case kErrorNotFound:
         challenge.mLength = 0;
@@ -2541,7 +2215,7 @@
         ExitNow(error = kErrorParse);
     }
 
-    tlvs[tlvslength++] = Tlv::kSourceAddress;
+    tlvList.Add(Tlv::kSourceAddress);
 
     aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(extAddr);
     child = mChildTable.FindChild(extAddr, Child::kInStateAnyExceptInvalid);
@@ -2552,8 +2226,8 @@
         // Status TLV (error).
         if (mode.IsRxOnWhenIdle())
         {
-            tlvs[tlvslength++] = Tlv::kStatus;
-            SendChildUpdateResponse(nullptr, aRxInfo.mMessageInfo, tlvs, tlvslength, challenge);
+            tlvList.Add(Tlv::kStatus);
+            SendChildUpdateResponse(nullptr, aRxInfo.mMessageInfo, tlvList, challenge);
         }
 
         ExitNow();
@@ -2570,22 +2244,27 @@
     oldMode = child->GetDeviceMode();
     child->SetDeviceMode(mode);
 
-    tlvs[tlvslength++] = Tlv::kMode;
+    tlvList.Add(Tlv::kMode);
 
     // Parent MUST include Leader Data TLV in Child Update Response
-    tlvs[tlvslength++] = Tlv::kLeaderData;
+    tlvList.Add(Tlv::kLeaderData);
 
     if (challenge.mLength != 0)
     {
-        tlvs[tlvslength++] = Tlv::kMleFrameCounter;
-        tlvs[tlvslength++] = Tlv::kLinkFrameCounter;
+        tlvList.Add(Tlv::kMleFrameCounter);
+        tlvList.Add(Tlv::kLinkFrameCounter);
     }
 
     // IPv6 Address TLV
-    if (Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kAddressRegistration, addressRegistrationOffset) == kErrorNone)
+    switch (ProcessAddressRegistrationTlv(aRxInfo, *child))
     {
-        SuccessOrExit(error = UpdateChildAddresses(aRxInfo.mMessage, addressRegistrationOffset, *child));
-        tlvs[tlvslength++] = Tlv::kAddressRegistration;
+    case kErrorNone:
+        tlvList.Add(Tlv::kAddressRegistration);
+        break;
+    case kErrorNotFound:
+        break;
+    default:
+        ExitNow(error = kErrorParse);
     }
 
     // Leader Data
@@ -2610,7 +2289,7 @@
             childDidChange = true;
         }
 
-        tlvs[tlvslength++] = Tlv::kTimeout;
+        tlvList.Add(Tlv::kTimeout);
         break;
 
     case kErrorNotFound:
@@ -2620,19 +2299,29 @@
         ExitNow(error = kErrorParse);
     }
 
-    // TLV Request
-    switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvs))
+    // Supervision interval
+    switch (Tlv::Find<SupervisionIntervalTlv>(aRxInfo.mMessage, supervisionInterval))
     {
     case kErrorNone:
-        VerifyOrExit(requestedTlvs.mNumTlvs <= (kMaxResponseTlvs - tlvslength), error = kErrorParse);
-        for (uint8_t i = 0; i < requestedTlvs.mNumTlvs; i++)
-        {
-            // Skip LeaderDataTlv since it is already included by default.
-            if (requestedTlvs.mTlvs[i] != Tlv::kLeaderData)
-            {
-                tlvs[tlvslength++] = requestedTlvs.mTlvs[i];
-            }
-        }
+        tlvList.Add(Tlv::kSupervisionInterval);
+        break;
+
+    case kErrorNotFound:
+        supervisionInterval =
+            (child->GetVersion() <= kThreadVersion1p3) ? kChildSupervisionDefaultIntervalForOlderVersion : 0;
+        break;
+
+    default:
+        ExitNow(error = kErrorParse);
+    }
+
+    child->SetSupervisionInterval(supervisionInterval);
+
+    // TLV Request
+    switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
+    {
+    case kErrorNone:
+        tlvList.AddElementsFrom(requestedTlvList);
         break;
     case kErrorNotFound:
         break;
@@ -2646,22 +2335,27 @@
         CslChannelTlv cslChannel;
         uint32_t      cslTimeout;
 
-        if (Tlv::Find<CslTimeoutTlv>(aRxInfo.mMessage, cslTimeout) == kErrorNone)
+        switch (Tlv::Find<CslTimeoutTlv>(aRxInfo.mMessage, cslTimeout))
         {
+        case kErrorNone:
             child->SetCslTimeout(cslTimeout);
             // MUST include CSL accuracy TLV when request includes CSL timeout
-            tlvs[tlvslength++] = Tlv::kCslClockAccuracy;
+            tlvList.Add(Tlv::kCslClockAccuracy);
+            break;
+        case kErrorNotFound:
+            break;
+        default:
+            ExitNow(error = kErrorNone);
         }
 
         if (Tlv::FindTlv(aRxInfo.mMessage, cslChannel) == kErrorNone)
         {
+            VerifyOrExit(cslChannel.IsValid(), error = kErrorParse);
+
+            // Special value of zero is used to indicate that
+            // CSL channel is not specified.
             child->SetCslChannel(static_cast<uint8_t>(cslChannel.GetChannel()));
         }
-        else
-        {
-            // Set CSL Channel unspecified.
-            child->SetCslChannel(0);
-        }
     }
 #endif // OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
 
@@ -2706,7 +2400,7 @@
     }
 #endif
 
-    SendChildUpdateResponse(child, aRxInfo.mMessageInfo, tlvs, tlvslength, challenge);
+    SendChildUpdateResponse(child, aRxInfo.mMessageInfo, tlvList, challenge);
 
     aRxInfo.mClass = RxInfo::kPeerMessage;
 
@@ -2724,10 +2418,10 @@
     uint32_t   linkFrameCounter;
     uint32_t   mleFrameCounter;
     LeaderData leaderData;
-    Child *    child;
-    uint16_t   addressRegistrationOffset = 0;
+    Child     *child;
 
-    if ((aRxInfo.mNeighbor == nullptr) || IsActiveRouter(aRxInfo.mNeighbor->GetRloc16()))
+    if ((aRxInfo.mNeighbor == nullptr) || IsActiveRouter(aRxInfo.mNeighbor->GetRloc16()) ||
+        !Get<ChildTable>().Contains(*aRxInfo.mNeighbor))
     {
         Log(kMessageReceive, kTypeChildUpdateResponseOfUnknownChild, aRxInfo.mMessageInfo.GetPeerAddr());
         ExitNow(error = kErrorNotFound);
@@ -2771,7 +2465,7 @@
     }
 
     // Status
-    switch (Tlv::Find<ThreadStatusTlv>(aRxInfo.mMessage, status))
+    switch (Tlv::Find<StatusTlv>(aRxInfo.mMessage, status))
     {
     case kErrorNone:
         VerifyOrExit(status != StatusTlv::kError, RemoveNeighbor(*child));
@@ -2820,10 +2514,29 @@
         ExitNow(error = kErrorParse);
     }
 
-    // IPv6 Address
-    if (Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kAddressRegistration, addressRegistrationOffset) == kErrorNone)
     {
-        SuccessOrExit(error = UpdateChildAddresses(aRxInfo.mMessage, addressRegistrationOffset, *child));
+        uint16_t supervisionInterval;
+
+        switch (Tlv::Find<SupervisionIntervalTlv>(aRxInfo.mMessage, supervisionInterval))
+        {
+        case kErrorNone:
+            child->SetSupervisionInterval(supervisionInterval);
+            break;
+        case kErrorNotFound:
+            break;
+        default:
+            ExitNow(error = kErrorParse);
+        }
+    }
+
+    // IPv6 Address
+    switch (ProcessAddressRegistrationTlv(aRxInfo, *child))
+    {
+    case kErrorNone:
+    case kErrorNotFound:
+        break;
+    default:
+        ExitNow(error = kErrorParse);
     }
 
     // Leader Data
@@ -2852,22 +2565,15 @@
 void MleRouter::HandleDataRequest(RxInfo &aRxInfo)
 {
     Error              error = kErrorNone;
-    RequestedTlvs      requestedTlvs;
+    TlvList            tlvList;
     MeshCoP::Timestamp timestamp;
-    uint8_t            tlvs[4];
-    uint8_t            numTlvs;
 
     Log(kMessageReceive, kTypeDataRequest, aRxInfo.mMessageInfo.GetPeerAddr());
 
-    VerifyOrExit(aRxInfo.mNeighbor && aRxInfo.mNeighbor->IsStateValid(), error = kErrorSecurity);
+    VerifyOrExit(aRxInfo.IsNeighborStateValid(), error = kErrorSecurity);
 
     // TLV Request
-    SuccessOrExit(error = aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvs));
-    VerifyOrExit(requestedTlvs.mNumTlvs <= sizeof(tlvs), error = kErrorParse);
-
-    memset(tlvs, Tlv::kInvalid, sizeof(tlvs));
-    memcpy(tlvs, requestedTlvs.mTlvs, requestedTlvs.mNumTlvs);
-    numTlvs = requestedTlvs.mNumTlvs;
+    SuccessOrExit(error = aRxInfo.mMessage.ReadTlvRequestTlv(tlvList));
 
     // Active Timestamp
     switch (Tlv::Find<ActiveTimestampTlv>(aRxInfo.mMessage, timestamp))
@@ -2881,7 +2587,7 @@
         OT_FALL_THROUGH;
 
     case kErrorNotFound:
-        tlvs[numTlvs++] = Tlv::kActiveDataset;
+        tlvList.Add(Tlv::kActiveDataset);
         break;
 
     default:
@@ -2900,7 +2606,7 @@
         OT_FALL_THROUGH;
 
     case kErrorNotFound:
-        tlvs[numTlvs++] = Tlv::kPendingDataset;
+        tlvList.Add(Tlv::kPendingDataset);
         break;
 
     default:
@@ -2909,7 +2615,7 @@
 
     aRxInfo.mClass = RxInfo::kPeerMessage;
 
-    SendDataResponse(aRxInfo.mMessageInfo.GetPeerAddr(), tlvs, numTlvs, 0, &aRxInfo.mMessage);
+    SendDataResponse(aRxInfo.mMessageInfo.GetPeerAddr(), tlvList, /* aDelay */ 0, &aRxInfo.mMessage);
 
 exit:
     LogProcessError(kTypeDataRequest, error);
@@ -2917,16 +2623,17 @@
 
 void MleRouter::HandleNetworkDataUpdateRouter(void)
 {
-    static const uint8_t tlvs[] = {Tlv::kNetworkData};
-    Ip6::Address         destination;
-    uint16_t             delay;
+    Ip6::Address destination;
+    uint16_t     delay;
+    TlvList      tlvList;
 
     VerifyOrExit(IsRouterOrLeader());
 
     destination.SetToLinkLocalAllNodesMulticast();
+    tlvList.Add(Tlv::kNetworkData);
 
     delay = IsLeader() ? 0 : Random::NonCrypto::GetUint16InRange(0, kUnsolicitedDataResponseJitter);
-    SendDataResponse(destination, tlvs, sizeof(tlvs), delay);
+    SendDataResponse(destination, tlvList, delay);
 
     SynchronizeChildNetworkData();
 
@@ -2988,26 +2695,22 @@
 void MleRouter::HandleDiscoveryRequest(RxInfo &aRxInfo)
 {
     Error                        error = kErrorNone;
-    Tlv                          tlv;
     MeshCoP::Tlv                 meshcopTlv;
-    MeshCoP::DiscoveryRequestTlv discoveryRequest;
+    MeshCoP::DiscoveryRequestTlv discoveryRequestTlv;
     MeshCoP::ExtendedPanId       extPanId;
     uint16_t                     offset;
+    uint16_t                     length;
     uint16_t                     end;
 
     Log(kMessageReceive, kTypeDiscoveryRequest, aRxInfo.mMessageInfo.GetPeerAddr());
 
-    discoveryRequest.SetLength(0);
+    discoveryRequestTlv.SetLength(0);
 
     // only Routers and REEDs respond
     VerifyOrExit(IsRouterEligible(), error = kErrorInvalidState);
 
-    // find MLE Discovery TLV
-    VerifyOrExit(Tlv::FindTlvOffset(aRxInfo.mMessage, Tlv::kDiscovery, offset) == kErrorNone, error = kErrorParse);
-    IgnoreError(aRxInfo.mMessage.Read(offset, tlv));
-
-    offset += sizeof(tlv);
-    end = offset + sizeof(tlv) + tlv.GetLength();
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kDiscovery, offset, length));
+    end = offset + length;
 
     while (offset < end)
     {
@@ -3016,8 +2719,8 @@
         switch (meshcopTlv.GetType())
         {
         case MeshCoP::Tlv::kDiscoveryRequest:
-            IgnoreError(aRxInfo.mMessage.Read(offset, discoveryRequest));
-            VerifyOrExit(discoveryRequest.IsValid(), error = kErrorParse);
+            IgnoreError(aRxInfo.mMessage.Read(offset, discoveryRequestTlv));
+            VerifyOrExit(discoveryRequestTlv.IsValid(), error = kErrorParse);
 
             break;
 
@@ -3034,20 +2737,20 @@
         offset += sizeof(meshcopTlv) + meshcopTlv.GetLength();
     }
 
-    if (discoveryRequest.IsValid())
+    if (discoveryRequestTlv.IsValid())
     {
-        if (mDiscoveryRequestCallback != nullptr)
+        if (mDiscoveryRequestCallback.IsSet())
         {
             otThreadDiscoveryRequestInfo info;
 
             aRxInfo.mMessageInfo.GetPeerAddr().GetIid().ConvertToExtAddress(AsCoreType(&info.mExtAddress));
-            info.mVersion  = discoveryRequest.GetVersion();
-            info.mIsJoiner = discoveryRequest.IsJoiner();
+            info.mVersion  = discoveryRequestTlv.GetVersion();
+            info.mIsJoiner = discoveryRequestTlv.IsJoiner();
 
-            mDiscoveryRequestCallback(&info, mDiscoveryRequestCallbackContext);
+            mDiscoveryRequestCallback.Invoke(&info);
         }
 
-        if (discoveryRequest.IsJoiner())
+        if (discoveryRequestTlv.IsJoiner())
         {
 #if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
             if (!mSteeringData.IsEmpty())
@@ -3070,14 +2773,15 @@
 Error MleRouter::SendDiscoveryResponse(const Ip6::Address &aDestination, const Message &aDiscoverRequestMessage)
 {
     Error                         error = kErrorNone;
-    TxMessage *                   message;
+    TxMessage                    *message;
     uint16_t                      startOffset;
     Tlv                           tlv;
-    MeshCoP::DiscoveryResponseTlv discoveryResponse;
-    MeshCoP::NetworkNameTlv       networkName;
+    MeshCoP::DiscoveryResponseTlv discoveryResponseTlv;
+    MeshCoP::NetworkNameTlv       networkNameTlv;
     uint16_t                      delay;
 
     VerifyOrExit((message = NewMleMessage(kCommandDiscoveryResponse)) != nullptr, error = kErrorNoBufs);
+    message->SetDirectTransmission();
     message->SetPanId(aDiscoverRequestMessage.GetPanId());
 #if OPENTHREAD_CONFIG_MULTI_RADIO
     // Send the MLE Discovery Response message on same radio link
@@ -3092,8 +2796,8 @@
     startOffset = message->GetLength();
 
     // Discovery Response TLV
-    discoveryResponse.Init();
-    discoveryResponse.SetVersion(kThreadVersion);
+    discoveryResponseTlv.Init();
+    discoveryResponseTlv.SetVersion(kThreadVersion);
 
 #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
     if (Get<KeyManager>().GetSecurityPolicy().mNativeCommissioningEnabled)
@@ -3101,29 +2805,29 @@
         SuccessOrExit(
             error = Tlv::Append<MeshCoP::CommissionerUdpPortTlv>(*message, Get<MeshCoP::BorderAgent>().GetUdpPort()));
 
-        discoveryResponse.SetNativeCommissioner(true);
+        discoveryResponseTlv.SetNativeCommissioner(true);
     }
     else
 #endif
     {
-        discoveryResponse.SetNativeCommissioner(false);
+        discoveryResponseTlv.SetNativeCommissioner(false);
     }
 
     if (Get<KeyManager>().GetSecurityPolicy().mCommercialCommissioningEnabled)
     {
-        discoveryResponse.SetCommercialCommissioningMode(true);
+        discoveryResponseTlv.SetCommercialCommissioningMode(true);
     }
 
-    SuccessOrExit(error = discoveryResponse.AppendTo(*message));
+    SuccessOrExit(error = discoveryResponseTlv.AppendTo(*message));
 
     // Extended PAN ID TLV
     SuccessOrExit(
         error = Tlv::Append<MeshCoP::ExtendedPanIdTlv>(*message, Get<MeshCoP::ExtendedPanIdManager>().GetExtPanId()));
 
     // Network Name TLV
-    networkName.Init();
-    networkName.SetNetworkName(Get<MeshCoP::NetworkNameManager>().GetNetworkName().GetAsData());
-    SuccessOrExit(error = networkName.AppendTo(*message));
+    networkNameTlv.Init();
+    networkNameTlv.SetNetworkName(Get<MeshCoP::NetworkNameManager>().GetNetworkName().GetAsData());
+    SuccessOrExit(error = networkNameTlv.AppendTo(*message));
 
 #if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
     // If steering data is set out of band, use that value.
@@ -3168,7 +2872,7 @@
 {
     Error        error = kErrorNone;
     Ip6::Address destination;
-    TxMessage *  message;
+    TxMessage   *message;
 
     VerifyOrExit((message = NewMleMessage(kCommandChildIdResponse)) != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = message->AppendSourceAddressTlv());
@@ -3227,7 +2931,7 @@
 
     if (!aChild.IsFullThreadDevice())
     {
-        SuccessOrExit(error = message->AppendAddresseRegisterationTlv(aChild));
+        SuccessOrExit(error = message->AppendAddressRegistrationTlv(aChild));
     }
 
     SetChildStateToValid(aChild);
@@ -3256,10 +2960,11 @@
 
 Error MleRouter::SendChildUpdateRequest(Child &aChild)
 {
-    static const uint8_t tlvs[] = {Tlv::kTimeout, Tlv::kAddressRegistration};
-    Error                error  = kErrorNone;
-    Ip6::Address         destination;
-    TxMessage *          message = nullptr;
+    static const uint8_t kTlvs[] = {Tlv::kTimeout, Tlv::kAddressRegistration};
+
+    Error        error = kErrorNone;
+    Ip6::Address destination;
+    TxMessage   *message = nullptr;
 
     if (!aChild.IsRxOnWhenIdle())
     {
@@ -3292,7 +2997,7 @@
 
     if (!aChild.IsStateValid())
     {
-        SuccessOrExit(error = message->AppendTlvRequestTlv(tlvs, sizeof(tlvs)));
+        SuccessOrExit(error = message->AppendTlvRequestTlv(kTlvs));
         aChild.GenerateChallenge();
         SuccessOrExit(error = message->AppendChallengeTlv(aChild.GetChallenge(), aChild.GetChallengeSize()));
     }
@@ -3313,33 +3018,61 @@
     return error;
 }
 
-void MleRouter::SendChildUpdateResponse(Child *                 aChild,
+void MleRouter::SendChildUpdateResponse(Child                  *aChild,
                                         const Ip6::MessageInfo &aMessageInfo,
-                                        const uint8_t *         aTlvs,
-                                        uint8_t                 aTlvsLength,
-                                        const Challenge &       aChallenge)
+                                        const TlvList          &aTlvList,
+                                        const Challenge        &aChallenge)
 {
     Error      error = kErrorNone;
     TxMessage *message;
 
     VerifyOrExit((message = NewMleMessage(kCommandChildUpdateResponse)) != nullptr, error = kErrorNoBufs);
 
-    for (int i = 0; i < aTlvsLength; i++)
+    for (uint8_t tlvType : aTlvList)
     {
-        switch (aTlvs[i])
+        // Add all TLV types that do not depend on `child`
+
+        switch (tlvType)
         {
         case Tlv::kStatus:
             SuccessOrExit(error = message->AppendStatusTlv(StatusTlv::kError));
             break;
 
-        case Tlv::kAddressRegistration:
-            SuccessOrExit(error = message->AppendAddresseRegisterationTlv(*aChild));
-            break;
-
         case Tlv::kLeaderData:
             SuccessOrExit(error = message->AppendLeaderDataTlv());
             break;
 
+        case Tlv::kResponse:
+            SuccessOrExit(error = message->AppendResponseTlv(aChallenge));
+            break;
+
+        case Tlv::kSourceAddress:
+            SuccessOrExit(error = message->AppendSourceAddressTlv());
+            break;
+
+        case Tlv::kMleFrameCounter:
+            SuccessOrExit(error = message->AppendMleFrameCounterTlv());
+            break;
+
+        case Tlv::kLinkFrameCounter:
+            SuccessOrExit(error = message->AppendLinkFrameCounterTlv());
+            break;
+        }
+
+        // Make sure `child` is not null before adding TLV types
+        // that can depend on it.
+
+        if (aChild == nullptr)
+        {
+            continue;
+        }
+
+        switch (tlvType)
+        {
+        case Tlv::kAddressRegistration:
+            SuccessOrExit(error = message->AppendAddressRegistrationTlv(*aChild));
+            break;
+
         case Tlv::kMode:
             SuccessOrExit(error = message->AppendModeTlv(aChild->GetDeviceMode()));
             break;
@@ -3350,24 +3083,12 @@
             SuccessOrExit(error = message->AppendPendingTimestampTlv());
             break;
 
-        case Tlv::kResponse:
-            SuccessOrExit(error = message->AppendResponseTlv(aChallenge));
-            break;
-
-        case Tlv::kSourceAddress:
-            SuccessOrExit(error = message->AppendSourceAddressTlv());
-            break;
-
         case Tlv::kTimeout:
             SuccessOrExit(error = message->AppendTimeoutTlv(aChild->GetTimeout()));
             break;
 
-        case Tlv::kMleFrameCounter:
-            SuccessOrExit(error = message->AppendMleFrameCounterTlv());
-            break;
-
-        case Tlv::kLinkFrameCounter:
-            SuccessOrExit(error = message->AppendLinkFrameCounterTlv());
+        case Tlv::kSupervisionInterval:
+            SuccessOrExit(error = message->AppendSupervisionIntervalTlv(aChild->GetSupervisionInterval()));
             break;
 
 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
@@ -3397,16 +3118,15 @@
 }
 
 void MleRouter::SendDataResponse(const Ip6::Address &aDestination,
-                                 const uint8_t *     aTlvs,
-                                 uint8_t             aTlvsLength,
+                                 const TlvList      &aTlvList,
                                  uint16_t            aDelay,
-                                 const Message *     aRequestMessage)
+                                 const Message      *aRequestMessage)
 {
     OT_UNUSED_VARIABLE(aRequestMessage);
 
     Error      error   = kErrorNone;
     TxMessage *message = nullptr;
-    Neighbor * neighbor;
+    Neighbor  *neighbor;
 
     if (mRetrieveNewNetworkData)
     {
@@ -3420,9 +3140,9 @@
     SuccessOrExit(error = message->AppendActiveTimestampTlv());
     SuccessOrExit(error = message->AppendPendingTimestampTlv());
 
-    for (int i = 0; i < aTlvsLength; i++)
+    for (uint8_t tlvType : aTlvList)
     {
-        switch (aTlvs[i])
+        switch (tlvType)
         {
         case Tlv::kNetworkData:
             neighbor = mNeighborTable.FindNeighbor(aDestination);
@@ -3438,12 +3158,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
         }
@@ -3498,12 +3222,10 @@
         }
         break;
 
-#if OPENTHREAD_FTD
     case kRoleRouter:
     case kRoleLeader:
         mRouterTable.RemoveRouterLink(aRouter);
         break;
-#endif
 
     default:
         break;
@@ -3521,13 +3243,13 @@
             IgnoreError(BecomeDetached());
         }
     }
-    else if (&aNeighbor == &mParentCandidate)
+    else if (&aNeighbor == &GetParentCandidate())
     {
-        mParentCandidate.Clear();
+        ClearParentCandidate();
     }
     else if (!IsActiveRouter(aNeighbor.GetRloc16()))
     {
-        OT_ASSERT(mChildTable.GetChildIndex(static_cast<Child &>(aNeighbor)) < kMaxChildren);
+        OT_ASSERT(mChildTable.Contains(aNeighbor));
 
         if (aNeighbor.IsStateValidOrRestoring())
         {
@@ -3539,7 +3261,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));
@@ -3562,82 +3284,6 @@
     return;
 }
 
-uint16_t MleRouter::GetNextHop(uint16_t aDestination)
-{
-    uint8_t       destinationId = RouterIdFromRloc16(aDestination);
-    uint8_t       routeCost;
-    uint8_t       linkCost;
-    uint16_t      rval = Mac::kShortAddrInvalid;
-    const Router *router;
-    const Router *nextHop;
-
-    if (IsChild())
-    {
-        ExitNow(rval = Mle::GetNextHop(aDestination));
-    }
-
-    // The frame is destined to a child
-    if (destinationId == mRouterId)
-    {
-        ExitNow(rval = aDestination);
-    }
-
-    router = mRouterTable.GetRouter(destinationId);
-    VerifyOrExit(router != nullptr);
-
-    linkCost  = GetLinkCost(destinationId);
-    routeCost = GetRouteCost(aDestination);
-
-    if ((routeCost + GetLinkCost(router->GetNextHop())) < linkCost)
-    {
-        nextHop = mRouterTable.GetRouter(router->GetNextHop());
-        VerifyOrExit(nextHop != nullptr && !nextHop->IsStateInvalid());
-
-        rval = Rloc16FromRouterId(router->GetNextHop());
-    }
-    else if (linkCost < kMaxRouteCost)
-    {
-        rval = Rloc16FromRouterId(destinationId);
-    }
-
-exit:
-    return rval;
-}
-
-uint8_t MleRouter::GetCost(uint16_t aRloc16)
-{
-    uint8_t routerId = RouterIdFromRloc16(aRloc16);
-    uint8_t cost     = GetLinkCost(routerId);
-    Router *router   = mRouterTable.GetRouter(routerId);
-    uint8_t routeCost;
-
-    VerifyOrExit(router != nullptr && mRouterTable.GetRouter(router->GetNextHop()) != nullptr);
-
-    routeCost = GetRouteCost(aRloc16) + GetLinkCost(router->GetNextHop());
-
-    if (cost > routeCost)
-    {
-        cost = routeCost;
-    }
-
-exit:
-    return cost;
-}
-
-uint8_t MleRouter::GetRouteCost(uint16_t aRloc16) const
-{
-    uint8_t       rval = kMaxRouteCost;
-    const Router *router;
-
-    router = mRouterTable.GetRouter(RouterIdFromRloc16(aRloc16));
-    VerifyOrExit(router != nullptr && mRouterTable.GetRouter(router->GetNextHop()) != nullptr);
-
-    rval = router->GetCost();
-
-exit:
-    return rval;
-}
-
 Error MleRouter::SetPreferredRouterId(uint8_t aRouterId)
 {
     Error error = kErrorNone;
@@ -3666,11 +3312,11 @@
     }
 
     // loop exists
-    router = mRouterTable.GetRouter(RouterIdFromRloc16(aDestRloc16));
+    router = mRouterTable.FindRouterByRloc16(aDestRloc16);
     VerifyOrExit(router != nullptr);
 
     // invalidate next hop
-    router->SetNextHop(kInvalidRouterId);
+    router->SetNextHopToInvalid();
     ResetAdvertiseInterval();
 
 exit:
@@ -3725,11 +3371,11 @@
 {
     Error            error = kErrorNone;
     Tmf::MessageInfo messageInfo(GetInstance());
-    Coap::Message *  message = nullptr;
+    Coap::Message   *message = nullptr;
 
     VerifyOrExit(!mAddressSolicitPending);
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kAddressSolicit);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriAddressSolicit);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = Tlv::Append<ThreadExtMacAddressTlv>(*message, Get<Mac::Mac>().GetExtAddress()));
@@ -3757,13 +3403,13 @@
     return error;
 }
 
-void MleRouter::SendAddressRelease(Coap::ResponseHandler aResponseHandler, void *aResponseHandlerContext)
+void MleRouter::SendAddressRelease(void)
 {
     Error            error = kErrorNone;
     Tmf::MessageInfo messageInfo(GetInstance());
-    Coap::Message *  message;
+    Coap::Message   *message;
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kAddressRelease);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriAddressRelease);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = Tlv::Append<ThreadRloc16Tlv>(*message, Rloc16FromRouterId(mRouterId)));
@@ -3771,8 +3417,7 @@
 
     SuccessOrExit(error = messageInfo.SetSockAddrToRlocPeerAddrToLeaderRloc());
 
-    SuccessOrExit(error =
-                      Get<Tmf::Agent>().SendMessage(*message, messageInfo, aResponseHandler, aResponseHandlerContext));
+    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
     Log(kMessageSend, kTypeAddressRelease, messageInfo.GetPeerAddr());
 
@@ -3781,8 +3426,8 @@
     LogSendError(kTypeAddressRelease, error);
 }
 
-void MleRouter::HandleAddressSolicitResponse(void *               aContext,
-                                             otMessage *          aMessage,
+void MleRouter::HandleAddressSolicitResponse(void                *aContext,
+                                             otMessage           *aMessage,
                                              const otMessageInfo *aMessageInfo,
                                              Error                aResult)
 {
@@ -3790,7 +3435,7 @@
                                                                      AsCoreTypePtr(aMessageInfo), aResult);
 }
 
-void MleRouter::HandleAddressSolicitResponse(Coap::Message *         aMessage,
+void MleRouter::HandleAddressSolicitResponse(Coap::Message          *aMessage,
                                              const Ip6::MessageInfo *aMessageInfo,
                                              Error                   aResult)
 {
@@ -3798,8 +3443,7 @@
     uint16_t            rloc16;
     ThreadRouterMaskTlv routerMaskTlv;
     uint8_t             routerId;
-    Router *            router;
-    Router *            leader;
+    Router             *router;
 
     mAddressSolicitPending = false;
 
@@ -3838,46 +3482,69 @@
     SetRouterId(routerId);
 
     SetStateRouter(Rloc16FromRouterId(mRouterId));
-    mRouterTable.Clear();
+
+    // We keep the router table next hop and cost as what we had as a
+    // REED, i.e., our parent was the next hop towards all other
+    // routers and we tracked its cost towards them. As FED, we may
+    // have established links with a subset of neighboring routers.
+    // We ensure to clear these links to avoid using them (since will
+    // be rejected by the neighbor).
+
+    mRouterTable.ClearNeighbors();
+
     mRouterTable.UpdateRouterIdSet(routerMaskTlv.GetIdSequence(), routerMaskTlv.GetAssignedRouterIdMask());
 
-    router = mRouterTable.GetRouter(routerId);
+    router = mRouterTable.FindRouterById(routerId);
     VerifyOrExit(router != nullptr);
-
     router->SetExtAddress(Get<Mac::Mac>().GetExtAddress());
-    router->SetCost(0);
+    router->SetNextHopToInvalid();
 
-    router = mRouterTable.GetRouter(mParent.GetRouterId());
+    // Ensure we have our parent as a neighboring router, copying the
+    // `mParent` entry.
+
+    router = mRouterTable.FindRouterById(mParent.GetRouterId());
     VerifyOrExit(router != nullptr);
-
-    // Keep link to the parent in order to respond to Parent Requests before new link is established.
-    *router = mParent;
+    router->SetFrom(mParent);
     router->SetState(Neighbor::kStateValid);
-    router->SetNextHop(kInvalidRouterId);
-    router->SetCost(0);
+    router->SetNextHopToInvalid();
 
-    leader = mRouterTable.GetLeader();
-    OT_ASSERT(leader != nullptr);
-
-    if (leader != router)
+    // Ensure we have a next hop and cost towards leader.
+    if (mRouterTable.GetPathCostToLeader() >= kMaxRouteCost)
     {
-        // Keep route path to the Leader reported by the parent before it is updated.
-        leader->SetCost(mParentLeaderCost);
-        leader->SetNextHop(RouterIdFromRloc16(mParent.GetRloc16()));
+        Router *leader = mRouterTable.GetLeader();
+
+        OT_ASSERT(leader != nullptr);
+        leader->SetNextHopAndCost(RouterIdFromRloc16(mParent.GetRloc16()), mParent.GetLeaderCost());
     }
 
+    // We send an Advertisement to inform our former parent of our
+    // newly allocated Router ID. This will cause the parent to
+    // reset its advertisement trickle timer which can help speed
+    // up the dissemination of the new Router ID to other routers.
+    // This can also help with quicker link establishment with our
+    // former parent and other routers.
+    SendAdvertisement();
+
     for (Child &child : Get<ChildTable>().Iterate(Child::kInStateChildIdRequest))
     {
         IgnoreError(SendChildIdResponse(child));
     }
 
-    mLinkRequestDelay = kMulticastLinkRequestDelay;
-
 exit:
     // Send announce after received address solicit reply if needed
     InformPreviousChannel();
 }
 
+Error MleRouter::SetChildRouterLinks(uint8_t aChildRouterLinks)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(IsDisabled(), error = kErrorInvalidState);
+    mChildRouterLinks = aChildRouterLinks;
+exit:
+    return error;
+}
+
 bool MleRouter::IsExpectedToBecomeRouterSoon(void) const
 {
     static constexpr uint8_t kMaxDelay = 10;
@@ -3887,20 +3554,17 @@
             mAddressSolicitPending);
 }
 
-void MleRouter::HandleAddressSolicit(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<MleRouter *>(aContext)->HandleAddressSolicit(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void MleRouter::HandleAddressSolicit(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void MleRouter::HandleTmf<kUriAddressSolicit>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error                   error          = kErrorNone;
     ThreadStatusTlv::Status responseStatus = ThreadStatusTlv::kNoAddressAvailable;
-    Router *                router         = nullptr;
+    Router                 *router         = nullptr;
     Mac::ExtAddress         extAddress;
     uint16_t                rloc16;
     uint8_t                 status;
 
+    VerifyOrExit(mRole == kRoleLeader, error = kErrorInvalidState);
+
     VerifyOrExit(aMessage.IsConfirmablePostRequest(), error = kErrorParse);
 
     Log(kMessageReceive, kTypeAddressSolicit, aMessageInfo.GetPeerAddr());
@@ -3929,7 +3593,7 @@
 #endif
 
     // Check if allocation already exists
-    router = mRouterTable.GetRouter(extAddress);
+    router = mRouterTable.FindRouter(extAddress);
 
     if (router != nullptr)
     {
@@ -3952,7 +3616,7 @@
             (Get<NetworkData::Leader>().CountBorderRouters(NetworkData::kRouterRoleOnly) >=
              kRouterUpgradeBorderRouterRequestThreshold))
         {
-            LogInfo("Rejecting BR %s router role req - have %d BR routers", extAddress.ToString().AsCString(),
+            LogInfo("Rejecting BR %s router role req - have %u BR routers", extAddress.ToString().AsCString(),
                     kRouterUpgradeBorderRouterRequestThreshold);
             ExitNow();
         }
@@ -3969,7 +3633,7 @@
 
         if (router != nullptr)
         {
-            LogInfo("Router id %d requested and provided!", RouterIdFromRloc16(rloc16));
+            LogInfo("Router id %u requested and provided!", RouterIdFromRloc16(rloc16));
         }
     }
 
@@ -3989,9 +3653,9 @@
     }
 }
 
-void MleRouter::SendAddressSolicitResponse(const Coap::Message &   aRequest,
+void MleRouter::SendAddressSolicitResponse(const Coap::Message    &aRequest,
                                            ThreadStatusTlv::Status aResponseStatus,
-                                           const Router *          aRouter,
+                                           const Router           *aRouter,
                                            const Ip6::MessageInfo &aMessageInfo)
 {
     Coap::Message *message = Get<Tmf::Agent>().NewPriorityResponseMessage(aRequest);
@@ -4008,7 +3672,7 @@
 
         routerMaskTlv.Init();
         routerMaskTlv.SetIdSequence(mRouterTable.GetRouterIdSequence());
-        routerMaskTlv.SetAssignedRouterIdMask(mRouterTable.GetRouterIdSet());
+        mRouterTable.GetRouterIdSet(routerMaskTlv.GetAssignedRouterIdMask());
 
         SuccessOrExit(routerMaskTlv.AppendTo(*message));
     }
@@ -4018,21 +3682,33 @@
 
     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);
 }
 
-void MleRouter::HandleAddressRelease(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<MleRouter *>(aContext)->HandleAddressRelease(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void MleRouter::HandleAddressRelease(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void MleRouter::HandleTmf<kUriAddressRelease>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     uint16_t        rloc16;
     Mac::ExtAddress extAddress;
     uint8_t         routerId;
-    Router *        router;
+    Router         *router;
+
+    VerifyOrExit(mRole == kRoleLeader);
 
     VerifyOrExit(aMessage.IsConfirmablePostRequest());
 
@@ -4042,7 +3718,7 @@
     SuccessOrExit(Tlv::Find<ThreadExtMacAddressTlv>(aMessage, extAddress));
 
     routerId = RouterIdFromRloc16(rloc16);
-    router   = mRouterTable.GetRouter(routerId);
+    router   = mRouterTable.FindRouterById(routerId);
 
     VerifyOrExit((router != nullptr) && (router->GetExtAddress() == extAddress));
 
@@ -4058,9 +3734,7 @@
 
 void MleRouter::FillConnectivityTlv(ConnectivityTlv &aTlv)
 {
-    Router *leader;
-    uint8_t cost;
-    int8_t  parentPriority = kParentPriorityMedium;
+    int8_t parentPriority = kParentPriorityMedium;
 
     if (mParentPriority != kParentPriorityUnspecified)
     {
@@ -4083,67 +3757,17 @@
 
     aTlv.SetParentPriority(parentPriority);
 
-    // compute leader cost and link qualities
     aTlv.SetLinkQuality1(0);
     aTlv.SetLinkQuality2(0);
     aTlv.SetLinkQuality3(0);
 
-    leader = mRouterTable.GetLeader();
-    cost   = (leader != nullptr) ? leader->GetCost() : static_cast<uint8_t>(kMaxRouteCost);
-
-    switch (mRole)
+    if (IsChild())
     {
-    case kRoleDisabled:
-    case kRoleDetached:
-        cost = static_cast<uint8_t>(kMaxRouteCost);
-        break;
-
-    case kRoleChild:
-        switch (mParent.GetLinkInfo().GetLinkQuality())
-        {
-        case kLinkQuality0:
-            break;
-
-        case kLinkQuality1:
-            aTlv.SetLinkQuality1(aTlv.GetLinkQuality1() + 1);
-            break;
-
-        case kLinkQuality2:
-            aTlv.SetLinkQuality2(aTlv.GetLinkQuality2() + 1);
-            break;
-
-        case kLinkQuality3:
-            aTlv.SetLinkQuality3(aTlv.GetLinkQuality3() + 1);
-            break;
-        }
-
-        cost += LinkQualityToCost(mParent.GetLinkInfo().GetLinkQuality());
-        break;
-
-    case kRoleRouter:
-        if (leader != nullptr)
-        {
-            cost += GetLinkCost(leader->GetNextHop());
-
-            if (!IsRouterIdValid(leader->GetNextHop()) || GetLinkCost(GetLeaderId()) < cost)
-            {
-                cost = GetLinkCost(GetLeaderId());
-            }
-        }
-
-        break;
-
-    case kRoleLeader:
-        cost = 0;
-        break;
+        aTlv.IncrementLinkQuality(mParent.GetLinkQualityIn());
     }
 
-    aTlv.SetActiveRouters(mRouterTable.GetActiveRouterCount());
-
-    for (Router &router : Get<RouterTable>().Iterate())
+    for (const Router &router : Get<RouterTable>())
     {
-        LinkQuality linkQuality;
-
         if (router.GetRloc16() == GetRloc16())
         {
             // skip self
@@ -4156,223 +3780,135 @@
             continue;
         }
 
-        linkQuality = router.GetLinkInfo().GetLinkQuality();
-
-        if (linkQuality > router.GetLinkQualityOut())
-        {
-            linkQuality = router.GetLinkQualityOut();
-        }
-
-        switch (linkQuality)
-        {
-        case kLinkQuality0:
-            break;
-
-        case kLinkQuality1:
-            aTlv.SetLinkQuality1(aTlv.GetLinkQuality1() + 1);
-            break;
-
-        case kLinkQuality2:
-            aTlv.SetLinkQuality2(aTlv.GetLinkQuality2() + 1);
-            break;
-
-        case kLinkQuality3:
-            aTlv.SetLinkQuality3(aTlv.GetLinkQuality3() + 1);
-            break;
-        }
+        aTlv.IncrementLinkQuality(router.GetTwoWayLinkQuality());
     }
 
-    aTlv.SetLeaderCost((cost < kMaxRouteCost) ? cost : static_cast<uint8_t>(kMaxRouteCost));
+    aTlv.SetActiveRouters(mRouterTable.GetActiveRouterCount());
+    aTlv.SetLeaderCost(Min(mRouterTable.GetPathCostToLeader(), kMaxRouteCost));
     aTlv.SetIdSequence(mRouterTable.GetRouterIdSequence());
     aTlv.SetSedBufferSize(OPENTHREAD_CONFIG_DEFAULT_SED_BUFFER_SIZE);
     aTlv.SetSedDatagramCount(OPENTHREAD_CONFIG_DEFAULT_SED_DATAGRAM_COUNT);
 }
 
-void MleRouter::FillRouteTlv(RouteTlv &aTlv, Neighbor *aNeighbor)
+bool MleRouter::ShouldDowngrade(uint8_t aNeighborId, const RouteTlv &aRouteTlv) const
 {
-    uint8_t     routerIdSequence = mRouterTable.GetRouterIdSequence();
-    RouterIdSet routerIdSet      = mRouterTable.GetRouterIdSet();
-    uint8_t     routerCount;
+    // Determine whether all conditions are satisfied for the router
+    // to downgrade after receiving info for a neighboring router
+    // with Router ID `aNeighborId` along with its `aRouteTlv`.
 
-    if (aNeighbor && IsActiveRouter(aNeighbor->GetRloc16()))
+    bool    shouldDowngrade   = false;
+    uint8_t activeRouterCount = mRouterTable.GetActiveRouterCount();
+    uint8_t count;
+
+    VerifyOrExit(IsRouter());
+    VerifyOrExit(mRouterTable.IsAllocated(aNeighborId));
+
+    // `mRouterSelectionJitterTimeout` is non-zero if we are already
+    // waiting to downgrade.
+
+    VerifyOrExit(mRouterSelectionJitterTimeout == 0);
+
+    VerifyOrExit(activeRouterCount > mRouterDowngradeThreshold);
+
+    // Check that we have at least `kMinDowngradeNeighbors`
+    // neighboring routers with two-way link quality of 2 or better.
+
+    count = 0;
+
+    for (const Router &router : mRouterTable)
     {
-        // Sending a Link Accept message that may require truncation
-        // of Route64 TLV
-
-        routerCount = mRouterTable.GetActiveRouterCount();
-
-        if (routerCount > kLinkAcceptMaxRouters)
-        {
-            for (uint8_t routerId = 0; routerId <= kMaxRouterId; routerId++)
-            {
-                if (routerCount <= kLinkAcceptMaxRouters)
-                {
-                    break;
-                }
-
-                if ((routerId == RouterIdFromRloc16(GetRloc16())) || (routerId == aNeighbor->GetRouterId()) ||
-                    (routerId == GetLeaderId()))
-                {
-                    // Route64 TLV must contain this device and the
-                    // neighboring router to ensure that at least this
-                    // link can be established.
-                    continue;
-                }
-
-                if (routerIdSet.Contains(routerId))
-                {
-                    routerIdSet.Remove(routerId);
-                    routerCount--;
-                }
-            }
-
-            // Ensure that the neighbor will process the current
-            // Route64 TLV in a subsequent message exchange
-            routerIdSequence -= kLinkAcceptSequenceRollback;
-        }
-    }
-
-    aTlv.SetRouterIdSequence(routerIdSequence);
-    aTlv.SetRouterIdMask(routerIdSet);
-
-    routerCount = 0;
-
-    for (Router &router : Get<RouterTable>().Iterate())
-    {
-        if (!routerIdSet.Contains(router.GetRouterId()))
+        if (!router.IsStateValid() || (router.GetTwoWayLinkQuality() < kLinkQuality2))
         {
             continue;
         }
 
-        if (router.GetRloc16() == GetRloc16())
+        count++;
+
+        if (count >= kMinDowngradeNeighbors)
         {
-            aTlv.SetLinkQualityIn(routerCount, kLinkQuality0);
-            aTlv.SetLinkQualityOut(routerCount, kLinkQuality0);
-            aTlv.SetRouteCost(routerCount, 1);
+            break;
         }
-        else
-        {
-            Router *nextHop;
-            uint8_t linkCost;
-            uint8_t routeCost;
-
-            linkCost = mRouterTable.GetLinkCost(router);
-            nextHop  = mRouterTable.GetRouter(router.GetNextHop());
-
-            if (nextHop == nullptr)
-            {
-                routeCost = linkCost;
-            }
-            else
-            {
-                routeCost = router.GetCost() + mRouterTable.GetLinkCost(*nextHop);
-
-                if (linkCost < routeCost)
-                {
-                    routeCost = linkCost;
-                }
-            }
-
-            if (routeCost >= kMaxRouteCost)
-            {
-                routeCost = 0;
-            }
-
-            aTlv.SetRouteCost(routerCount, routeCost);
-            aTlv.SetLinkQualityOut(routerCount, router.GetLinkQualityOut());
-            aTlv.SetLinkQualityIn(routerCount, router.GetLinkInfo().GetLinkQuality());
-        }
-
-        routerCount++;
     }
 
-    aTlv.SetRouteDataLength(routerCount);
+    VerifyOrExit(count >= kMinDowngradeNeighbors);
+
+    // Check that we have fewer children than three times the number
+    // of excess routers (defined as the difference between number of
+    // active routers and `mRouterDowngradeThreshold`).
+
+    count = activeRouterCount - mRouterDowngradeThreshold;
+    VerifyOrExit(mChildTable.GetNumChildren(Child::kInStateValid) < count * 3);
+
+    // Check that the neighbor has as good or better-quality links to
+    // same routers.
+
+    VerifyOrExit(NeighborHasComparableConnectivity(aRouteTlv, aNeighborId));
+
+#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTER_REQUEST_ROUTER_ROLE
+    // Check if we are eligible to be router due to being a BR.
+    VerifyOrExit(!Get<NetworkData::Notifier>().IsEligibleForRouterRoleUpgradeAsBorderRouter());
+#endif
+
+    shouldDowngrade = true;
+
+exit:
+    return shouldDowngrade;
 }
 
-bool MleRouter::HasMinDowngradeNeighborRouters(void)
+bool MleRouter::NeighborHasComparableConnectivity(const RouteTlv &aRouteTlv, uint8_t aNeighborId) const
 {
-    uint8_t linkQuality;
-    uint8_t routerCount = 0;
+    // Check whether the neighboring router with Router ID `aNeighborId`
+    // (along with its `aRouteTlv`) has as good or better-quality links
+    // to all our neighboring routers which have a two-way link quality
+    // of two or better.
 
-    for (Router &router : Get<RouterTable>().Iterate())
+    bool isComparable = true;
+
+    for (uint8_t routerId = 0, index = 0; routerId <= kMaxRouterId;
+         index += aRouteTlv.IsRouterIdSet(routerId) ? 1 : 0, routerId++)
     {
-        if (!router.IsStateValid())
+        const Router *router;
+        LinkQuality   localLinkQuality;
+        LinkQuality   peerLinkQuality;
+
+        if ((routerId == mRouterId) || (routerId == aNeighborId))
         {
             continue;
         }
 
-        linkQuality = router.GetLinkInfo().GetLinkQuality();
+        router = mRouterTable.FindRouterById(routerId);
 
-        if (linkQuality > router.GetLinkQualityOut())
+        if ((router == nullptr) || !router->IsStateValid())
         {
-            linkQuality = router.GetLinkQualityOut();
-        }
-
-        if (linkQuality >= 2)
-        {
-            routerCount++;
-        }
-    }
-
-    return routerCount >= kMinDowngradeNeighbors;
-}
-
-bool MleRouter::HasOneNeighborWithComparableConnectivity(const RouteTlv &aRoute, uint8_t aRouterId)
-{
-    uint8_t routerCount = 0;
-    bool    rval        = true;
-
-    // process local neighbor routers
-    for (Router &router : Get<RouterTable>().Iterate())
-    {
-        uint8_t localLinkQuality;
-        uint8_t peerLinkQuality;
-
-        if (!router.IsStateValid() || router.GetRouterId() == mRouterId || router.GetRouterId() == aRouterId)
-        {
-            routerCount++;
             continue;
         }
 
-        localLinkQuality = router.GetLinkInfo().GetLinkQuality();
+        localLinkQuality = router->GetTwoWayLinkQuality();
 
-        if (localLinkQuality > router.GetLinkQualityOut())
+        if (localLinkQuality < kLinkQuality2)
         {
-            localLinkQuality = router.GetLinkQualityOut();
-        }
-
-        if (localLinkQuality < 2)
-        {
-            routerCount++;
             continue;
         }
 
-        // check if this neighbor router is in peer Route64 TLV
-        if (!aRoute.IsRouterIdSet(router.GetRouterId()))
+        // `router` is our neighbor with two-way link quality of
+        // at least two. Check that `aRouteTlv` has as good or
+        // better-quality link to it as well.
+
+        if (!aRouteTlv.IsRouterIdSet(routerId))
         {
-            ExitNow(rval = false);
+            ExitNow(isComparable = false);
         }
 
-        // get the peer's two-way link quality to this router
-        peerLinkQuality = aRoute.GetLinkQualityIn(routerCount);
+        peerLinkQuality = Min(aRouteTlv.GetLinkQualityIn(index), aRouteTlv.GetLinkQualityOut(index));
 
-        if (peerLinkQuality > aRoute.GetLinkQualityOut(routerCount))
+        if (peerLinkQuality < localLinkQuality)
         {
-            peerLinkQuality = aRoute.GetLinkQualityOut(routerCount);
+            ExitNow(isComparable = false);
         }
-
-        // compare local link quality to this router with peer's
-        if (peerLinkQuality >= localLinkQuality)
-        {
-            routerCount++;
-            continue;
-        }
-
-        ExitNow(rval = false);
     }
 
 exit:
-    return rval;
+    return isComparable;
 }
 
 void MleRouter::SetChildStateToValid(Child &aChild)
@@ -4382,7 +3918,7 @@
     aChild.SetState(Neighbor::kStateValid);
     IgnoreError(mChildTable.StoreChild(aChild));
 
-#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
+#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
     Get<MlrManager>().UpdateProxiedSubscriptions(aChild, nullptr, 0);
 #endif
 
@@ -4392,10 +3928,7 @@
     return;
 }
 
-bool MleRouter::HasChildren(void)
-{
-    return mChildTable.HasChildren(Child::kInStateValidOrAttaching);
-}
+bool MleRouter::HasChildren(void) { return mChildTable.HasChildren(Child::kInStateValidOrAttaching); }
 
 void MleRouter::RemoveChildren(void)
 {
@@ -4405,21 +3938,6 @@
     }
 }
 
-bool MleRouter::HasSmallNumberOfChildren(void)
-{
-    uint16_t numChildren = 0;
-    uint8_t  routerCount = mRouterTable.GetActiveRouterCount();
-
-    VerifyOrExit(routerCount > mRouterDowngradeThreshold);
-
-    numChildren = mChildTable.GetNumChildren(Child::kInStateValid);
-
-    return numChildren < (routerCount - mRouterDowngradeThreshold) * 3;
-
-exit:
-    return false;
-}
-
 Error MleRouter::SetAssignParentPriority(int8_t aParentPriority)
 {
     Error error = kErrorNone;
@@ -4465,7 +3983,7 @@
 {
     Log(kMessageReceive, kTypeTimeSync, aRxInfo.mMessageInfo.GetPeerAddr());
 
-    VerifyOrExit(aRxInfo.mNeighbor && aRxInfo.mNeighbor->IsStateValid());
+    VerifyOrExit(aRxInfo.IsNeighborStateValid());
 
     aRxInfo.mClass = RxInfo::kPeerMessage;
 
@@ -4479,7 +3997,7 @@
 {
     Error        error = kErrorNone;
     Ip6::Address destination;
-    TxMessage *  message = nullptr;
+    TxMessage   *message = nullptr;
 
     VerifyOrExit((message = NewMleMessage(kCommandTimeSync)) != nullptr, error = kErrorNoBufs);
 
diff --git a/src/core/thread/mle_router.hpp b/src/core/thread/mle_router.hpp
index e0c7778..ac84f75 100644
--- a/src/core/thread/mle_router.hpp
+++ b/src/core/thread/mle_router.hpp
@@ -38,8 +38,8 @@
 
 #include <openthread/thread_ftd.h>
 
-#include "coap/coap.hpp"
 #include "coap/coap_message.hpp"
+#include "common/callback.hpp"
 #include "common/time_ticker.hpp"
 #include "common/timer.hpp"
 #include "common/trickle_timer.hpp"
@@ -52,6 +52,7 @@
 #include "thread/mle_tlvs.hpp"
 #include "thread/router_table.hpp"
 #include "thread/thread_tlvs.hpp"
+#include "thread/tmf.hpp"
 #include "thread/topology.hpp"
 
 namespace ot {
@@ -77,6 +78,7 @@
     friend class Mle;
     friend class ot::Instance;
     friend class ot::TimeTicker;
+    friend class Tmf::Agent;
 
 public:
     /**
@@ -117,7 +119,7 @@
      * @retval FALSE  It is a child or is not a single router in the network.
      *
      */
-    bool IsSingleton(void);
+    bool IsSingleton(void) const;
 
     /**
      * This method generates an Address Solicit request for a Router ID.
@@ -142,6 +144,22 @@
     Error BecomeLeader(void);
 
     /**
+     * This method gets the device properties which are used to determine the Leader Weight.
+     *
+     * @returns The current device properties.
+     *
+     */
+    const DeviceProperties &GetDeviceProperties(void) const { return mDeviceProperties; }
+
+    /**
+     * This method sets the device properties which are then used to determine and set the Leader Weight.
+     *
+     * @param[in]  aDeviceProperties    The device properties.
+     *
+     */
+    void SetDeviceProperties(const DeviceProperties &aDeviceProperties);
+
+    /**
      * This method returns the Leader Weighting value for this Thread interface.
      *
      * @returns The Leader Weighting value for this Thread interface.
@@ -152,6 +170,9 @@
     /**
      * This method sets the Leader Weighting value for this Thread interface.
      *
+     * This method directly sets the Leader Weight to the new value replacing its previous value (which may have been
+     * determined from a previous call to `SetDeviceProperties()`).
+     *
      * @param[in]  aWeight  The Leader Weighting value.
      *
      */
@@ -220,7 +241,7 @@
      * @returns A RLOC16 of the next hop if a route is known, kInvalidRloc16 otherwise.
      *
      */
-    uint16_t GetNextHop(uint16_t aDestination);
+    uint16_t GetNextHop(uint16_t aDestination) { return mRouterTable.GetNextHop(aDestination); }
 
     /**
      * This method returns the NETWORK_ID_TIMEOUT value.
@@ -239,36 +260,6 @@
     void SetNetworkIdTimeout(uint8_t aTimeout) { mNetworkIdTimeout = aTimeout; }
 
     /**
-     * This method returns the route cost to a RLOC16.
-     *
-     * @param[in]  aRloc16  The RLOC16 of the destination.
-     *
-     * @returns The route cost to a RLOC16.
-     *
-     */
-    uint8_t GetRouteCost(uint16_t aRloc16) const;
-
-    /**
-     * This method returns the link cost to the given Router.
-     *
-     * @param[in]  aRouterId  The Router ID.
-     *
-     * @returns The link cost to the Router.
-     *
-     */
-    uint8_t GetLinkCost(uint8_t aRouterId);
-
-    /**
-     * This method returns the minimum cost to the given router.
-     *
-     * @param[in]  aRloc16  The short address of the given router.
-     *
-     * @returns The minimum cost to the given router (via direct link or forwarding).
-     *
-     */
-    uint8_t GetCost(uint16_t aRloc16);
-
-    /**
      * This method returns the ROUTER_SELECTION_JITTER value.
      *
      * @returns The ROUTER_SELECTION_JITTER value.
@@ -325,6 +316,24 @@
     void SetRouterDowngradeThreshold(uint8_t aThreshold) { mRouterDowngradeThreshold = aThreshold; }
 
     /**
+     * This method returns the MLE_CHILD_ROUTER_LINKS value.
+     *
+     * @returns The MLE_CHILD_ROUTER_LINKS value.
+     *
+     */
+    uint8_t GetChildRouterLinks(void) const { return mChildRouterLinks; }
+
+    /**
+     * This method sets the MLE_CHILD_ROUTER_LINKS value.
+     *
+     * @param[in]  aChildRouterLinks  The MLE_CHILD_ROUTER_LINKS value.
+     *
+     * @retval kErrorNone          Successfully set the value.
+     * @retval kErrorInvalidState  Thread protocols are enabled.
+     */
+    Error SetChildRouterLinks(uint8_t aChildRouterLinks);
+
+    /**
      * This method returns if the REED is expected to become Router soon.
      *
      * @retval TRUE   If the REED is going to become a Router soon.
@@ -419,14 +428,6 @@
     void FillConnectivityTlv(ConnectivityTlv &aTlv);
 
     /**
-     * This method fills an RouteTlv.
-     *
-     * @param[out]  aTlv  A reference to the tlv to be filled.
-     *
-     */
-    void FillRouteTlv(RouteTlv &aTlv, Neighbor *aNeighbor = nullptr);
-
-    /**
      * This method generates an MLE Child Update Request message to be sent to the parent.
      *
      * @retval kErrorNone     Successfully generated an MLE Child Update Request message.
@@ -490,8 +491,7 @@
      */
     void SetDiscoveryRequestCallback(otThreadDiscoveryRequestCallback aCallback, void *aContext)
     {
-        mDiscoveryRequestCallback        = aCallback;
-        mDiscoveryRequestCallbackContext = aContext;
+        mDiscoveryRequestCallback.Set(aCallback, aContext);
     }
 
     /**
@@ -500,16 +500,6 @@
      */
     void ResetAdvertiseInterval(void);
 
-    /**
-     * This static method converts link quality to route cost.
-     *
-     * @param[in]  aLinkQuality  The link quality.
-     *
-     * @returns The link cost corresponding to @p aLinkQuality.
-     *
-     */
-    static uint8_t LinkQualityToCost(uint8_t aLinkQuality);
-
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
     /**
      * This method generates an MLE Time Synchronization message.
@@ -570,32 +560,33 @@
     void SetThreadVersionCheckEnabled(bool aEnabled) { mThreadVersionCheckEnabled = aEnabled; }
 #endif
 
-    /**
-     * This function sends an Address Release.
-     *
-     * @param[in] aResponseHandler        A pointer to a function that is called upon response reception or time-out.
-     * @param[in] aResponseHandlerContext A pointer to callback application-specific context.
-     *
-     */
-    void SendAddressRelease(Coap::ResponseHandler aResponseHandler = nullptr, void *aResponseHandlerContext = nullptr);
-
 private:
-    static constexpr uint16_t kDiscoveryMaxJitter            = 250;  // Max jitter delay Discovery Responses (in msec).
-    static constexpr uint32_t kStateUpdatePeriod             = 1000; // State update period (in msec).
-    static constexpr uint16_t kUnsolicitedDataResponseJitter = 500;  // Max delay for unsol Data Response (in msec).
+    static constexpr uint16_t kDiscoveryMaxJitter            = 250; // Max jitter delay Discovery Responses (in msec).
+    static constexpr uint16_t kChallengeTimeout              = 2;   // Challenge timeout (in sec).
+    static constexpr uint16_t kUnsolicitedDataResponseJitter = 500; // Max delay for unsol Data Response (in msec).
 
     // Threshold to accept a router upgrade request with reason
     // `kBorderRouterRequest` (number of BRs acting as router in
     // Network Data).
     static constexpr uint8_t kRouterUpgradeBorderRouterRequestThreshold = 2;
 
+    static constexpr uint8_t kLinkRequestMinMargin    = OPENTHREAD_CONFIG_MLE_LINK_REQUEST_MARGIN_MIN;
+    static constexpr uint8_t kPartitionMergeMinMargin = OPENTHREAD_CONFIG_MLE_PARTITION_MERGE_MARGIN_MIN;
+    static constexpr uint8_t kChildRouterLinks        = OPENTHREAD_CONFIG_MLE_CHILD_ROUTER_LINKS;
+    static constexpr uint8_t kMaxChildIpAddresses     = OPENTHREAD_CONFIG_MLE_IP_ADDRS_PER_CHILD;
+
+    static constexpr uint8_t kMinCriticalChildrenCount = 6;
+
+    static constexpr uint16_t kChildSupervisionDefaultIntervalForOlderVersion =
+        OPENTHREAD_CONFIG_CHILD_SUPERVISION_OLDER_VERSION_CHILD_DEFAULT_INTERVAL;
+
     void  HandleDetachStart(void);
     void  HandleChildStart(AttachMode aMode);
     void  HandleLinkRequest(RxInfo &aRxInfo);
     void  HandleLinkAccept(RxInfo &aRxInfo);
     Error HandleLinkAccept(RxInfo &aRxInfo, bool aRequest);
     void  HandleLinkAcceptAndRequest(RxInfo &aRxInfo);
-    Error HandleAdvertisement(RxInfo &aRxInfo);
+    Error HandleAdvertisement(RxInfo &aRxInfo, uint16_t aSourceAddress, const LeaderData &aLeaderData);
     void  HandleParentRequest(RxInfo &aRxInfo);
     void  HandleChildIdRequest(RxInfo &aRxInfo);
     void  HandleChildUpdateRequest(RxInfo &aRxInfo);
@@ -607,62 +598,57 @@
     void HandleTimeSync(RxInfo &aRxInfo);
 #endif
 
-    Error ProcessRouteTlv(RxInfo &aRxInfo);
-    Error ProcessRouteTlv(RxInfo &aRxInfo, RouteTlv &aRouteTlv);
+    Error ProcessRouteTlv(const RouteTlv &aRouteTlv, RxInfo &aRxInfo);
+    Error ReadAndProcessRouteTlvOnFed(RxInfo &aRxInfo, uint8_t aParentId);
+
     void  StopAdvertiseTrickleTimer(void);
     Error SendAddressSolicit(ThreadStatusTlv::Status aStatus);
-    void  SendAddressSolicitResponse(const Coap::Message &   aRequest,
+    void  SendAddressSolicitResponse(const Coap::Message    &aRequest,
                                      ThreadStatusTlv::Status aResponseStatus,
-                                     const Router *          aRouter,
+                                     const Router           *aRouter,
                                      const Ip6::MessageInfo &aMessageInfo);
+    void  SendAddressRelease(void);
     void  SendAdvertisement(void);
     Error SendLinkAccept(const Ip6::MessageInfo &aMessageInfo,
-                         Neighbor *              aNeighbor,
-                         const RequestedTlvs &   aRequestedTlvs,
-                         const Challenge &       aChallenge);
+                         Neighbor               *aNeighbor,
+                         const TlvList          &aRequestedTlvList,
+                         const Challenge        &aChallenge);
     void  SendParentResponse(Child *aChild, const Challenge &aChallenge, bool aRoutersOnlyRequest);
     Error SendChildIdResponse(Child &aChild);
     Error SendChildUpdateRequest(Child &aChild);
-    void  SendChildUpdateResponse(Child *                 aChild,
+    void  SendChildUpdateResponse(Child                  *aChild,
                                   const Ip6::MessageInfo &aMessageInfo,
-                                  const uint8_t *         aTlvs,
-                                  uint8_t                 aTlvsLength,
-                                  const Challenge &       aChallenge);
+                                  const TlvList          &aTlvList,
+                                  const Challenge        &aChallenge);
     void  SendDataResponse(const Ip6::Address &aDestination,
-                           const uint8_t *     aTlvs,
-                           uint8_t             aTlvsLength,
+                           const TlvList      &aTlvList,
                            uint16_t            aDelay,
-                           const Message *     aRequestMessage = nullptr);
+                           const Message      *aRequestMessage = nullptr);
     Error SendDiscoveryResponse(const Ip6::Address &aDestination, const Message &aDiscoverRequestMessage);
     void  SetStateRouter(uint16_t aRloc16);
     void  SetStateLeader(uint16_t aRloc16, LeaderStartMode aStartMode);
+    void  SetStateRouterOrLeader(DeviceRole aRole, uint16_t aRloc16, LeaderStartMode aStartMode);
     void  StopLeader(void);
     void  SynchronizeChildNetworkData(void);
-    Error UpdateChildAddresses(const Message &aMessage, uint16_t aOffset, Child &aChild);
-    void  UpdateRoutes(const RouteTlv &aRoute, uint8_t aRouterId);
-    bool  UpdateLinkQualityOut(const RouteTlv &aRoute, Router &aNeighbor, bool &aResetAdvInterval);
+    Error ProcessAddressRegistrationTlv(RxInfo &aRxInfo, Child &aChild);
+    Error UpdateChildAddresses(const Message &aMessage, uint16_t aOffset, uint16_t aLength, Child &aChild);
     bool  HasNeighborWithGoodLinkQuality(void) const;
 
-    static void HandleAddressSolicitResponse(void *               aContext,
-                                             otMessage *          aMessage,
+    static void HandleAddressSolicitResponse(void                *aContext,
+                                             otMessage           *aMessage,
                                              const otMessageInfo *aMessageInfo,
                                              Error                aResult);
     void HandleAddressSolicitResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult);
-    static void HandleAddressRelease(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleAddressRelease(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-    static void HandleAddressSolicit(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleAddressSolicit(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static bool IsSingleton(const RouteTlv &aRouteTlv);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
     void HandlePartitionChange(void);
 
     void SetChildStateToValid(Child &aChild);
     bool HasChildren(void);
     void RemoveChildren(void);
-    bool HasMinDowngradeNeighborRouters(void);
-    bool HasOneNeighborWithComparableConnectivity(const RouteTlv &aRoute, uint8_t aRouterId);
-    bool HasSmallNumberOfChildren(void);
+    bool ShouldDowngrade(uint8_t aNeighborId, const RouteTlv &aRouteTlv) const;
+    bool NeighborHasComparableConnectivity(const RouteTlv &aRouteTlv, uint8_t aNeighborId) const;
 
     static void HandleAdvertiseTrickleTimer(TrickleTimer &aTimer);
     void        HandleAdvertiseTrickleTimer(void);
@@ -670,8 +656,7 @@
 
     TrickleTimer mAdvertiseTrickleTimer;
 
-    Coap::Resource mAddressSolicit;
-    Coap::Resource mAddressRelease;
+    DeviceProperties mDeviceProperties;
 
     ChildTable  mChildTable;
     RouterTable mRouterTable;
@@ -704,7 +689,7 @@
     uint8_t mRouterSelectionJitter;        ///< The variable to save the assigned jitter value.
     uint8_t mRouterSelectionJitterTimeout; ///< The Timeout prior to request/release Router ID.
 
-    uint8_t mLinkRequestDelay;
+    uint8_t mChildRouterLinks;
 
     int8_t mParentPriority; ///< The assigned parent priority value, -2 means not assigned.
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
@@ -718,10 +703,12 @@
     MeshCoP::SteeringData mSteeringData;
 #endif
 
-    otThreadDiscoveryRequestCallback mDiscoveryRequestCallback;
-    void *                           mDiscoveryRequestCallbackContext;
+    Callback<otThreadDiscoveryRequestCallback> mDiscoveryRequestCallback;
 };
 
+DeclareTmfHandler(MleRouter, kUriAddressSolicit);
+DeclareTmfHandler(MleRouter, kUriAddressRelease);
+
 #endif // OPENTHREAD_FTD
 
 #if OPENTHREAD_MTD
@@ -741,8 +728,6 @@
 
     uint16_t GetNextHop(uint16_t aDestination) const { return Mle::GetNextHop(aDestination); }
 
-    uint8_t GetCost(uint16_t) { return 0; }
-
     Error RemoveNeighbor(Neighbor &) { return BecomeDetached(); }
     void  RemoveRouterLink(Router &) { IgnoreError(BecomeDetached()); }
 
diff --git a/src/core/thread/mle_tlvs.cpp b/src/core/thread/mle_tlvs.cpp
new file mode 100644
index 0000000..1908d61
--- /dev/null
+++ b/src/core/thread/mle_tlvs.cpp
@@ -0,0 +1,98 @@
+/*
+ *  Copyright (c) 2022, 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 function for generating and processing MLE TLVs.
+ */
+
+#include "mle_tlvs.hpp"
+
+#include "common/code_utils.hpp"
+
+namespace ot {
+namespace Mle {
+
+#if !OPENTHREAD_CONFIG_MLE_LONG_ROUTES_ENABLE
+
+void RouteTlv::Init(void)
+{
+    SetType(kRoute);
+    SetLength(sizeof(*this) - sizeof(Tlv));
+    mRouterIdMask.Clear();
+    memset(mRouteData, 0, sizeof(mRouteData));
+}
+
+bool RouteTlv::IsValid(void) const
+{
+    bool    isValid = false;
+    uint8_t numAllocatedIds;
+
+    VerifyOrExit(GetLength() >= sizeof(mRouterIdSequence) + sizeof(mRouterIdMask));
+
+    numAllocatedIds = mRouterIdMask.GetNumberOfAllocatedIds();
+    VerifyOrExit(numAllocatedIds <= Mle::kMaxRouters);
+
+    isValid = (GetRouteDataLength() >= numAllocatedIds);
+
+exit:
+    return isValid;
+}
+
+#endif // #if !OPENTHREAD_CONFIG_MLE_LONG_ROUTES_ENABLE
+
+void ConnectivityTlv::IncrementLinkQuality(LinkQuality aLinkQuality)
+{
+    switch (aLinkQuality)
+    {
+    case kLinkQuality0:
+        break;
+    case kLinkQuality1:
+        mLinkQuality1++;
+        break;
+    case kLinkQuality2:
+        mLinkQuality2++;
+        break;
+    case kLinkQuality3:
+        mLinkQuality3++;
+        break;
+    }
+}
+
+int8_t ConnectivityTlv::GetParentPriority(void) const
+{
+    return Preference::From2BitUint(mFlags >> kFlagsParentPriorityOffset);
+}
+
+void ConnectivityTlv::SetParentPriority(int8_t aParentPriority)
+{
+    mFlags = static_cast<uint8_t>(Preference::To2BitUint(aParentPriority) << kFlagsParentPriorityOffset);
+}
+
+} // namespace Mle
+} // namespace ot
diff --git a/src/core/thread/mle_tlvs.hpp b/src/core/thread/mle_tlvs.hpp
index 4b1dd67..7449965 100644
--- a/src/core/thread/mle_tlvs.hpp
+++ b/src/core/thread/mle_tlvs.hpp
@@ -38,6 +38,7 @@
 
 #include "common/encoding.hpp"
 #include "common/message.hpp"
+#include "common/preference.hpp"
 #include "common/tlvs.hpp"
 #include "meshcop/timestamp.hpp"
 #include "net/ip6_address.hpp"
@@ -102,6 +103,7 @@
         kActiveDataset         = 24, ///< Active Operational Dataset TLV
         kPendingDataset        = 25, ///< Pending Operational Dataset TLV
         kDiscovery             = 26, ///< Thread Discovery TLV
+        kSupervisionInterval   = 27, ///< Supervision Interval TLV
         kCslChannel            = 80, ///< CSL Channel TLV
         kCslTimeout            = 85, ///< CSL Timeout TLV
         kCslClockAccuracy      = 86, ///< CSL Clock Accuracy TLV
@@ -231,6 +233,12 @@
 typedef SimpleTlvInfo<Tlv::kPendingTimestamp, MeshCoP::Timestamp> PendingTimestampTlv;
 
 /**
+ * This class defines Timeout TLV constants and types.
+ *
+ */
+typedef UintTlvInfo<Tlv::kSupervisionInterval, uint16_t> SupervisionIntervalTlv;
+
+/**
  * This class defines CSL Timeout TLV constants and types.
  *
  */
@@ -256,13 +264,7 @@
      * This method initializes the TLV.
      *
      */
-    void Init(void)
-    {
-        SetType(kRoute);
-        SetLength(sizeof(*this) - sizeof(Tlv));
-        mRouterIdMask.Clear();
-        memset(mRouteData, 0, sizeof(mRouteData));
-    }
+    void Init(void);
 
     /**
      * This method indicates whether or not the TLV appears to be well-formed.
@@ -271,7 +273,7 @@
      * @retval FALSE  If the TLV does not appear to be well-formed.
      *
      */
-    bool IsValid(void) const { return GetLength() >= sizeof(mRouterIdSequence) + sizeof(mRouterIdMask); }
+    bool IsValid(void) const;
 
     /**
      * This method returns the Router ID Sequence value.
@@ -315,6 +317,15 @@
     bool IsRouterIdSet(uint8_t aRouterId) const { return mRouterIdMask.Contains(aRouterId); }
 
     /**
+     * This method indicates whether the `RouteTlv` is a singleton, i.e., only one router is allocated.
+     *
+     * @retval TRUE   It is a singleton.
+     * @retval FALSE  It is not a singleton.
+     *
+     */
+    bool IsSingleton(void) const { return IsValid() && (mRouterIdMask.GetNumberOfAllocatedIds() <= 1); }
+
+    /**
      * This method returns the Route Data Length value.
      *
      * @returns The Route Data Length value.
@@ -341,18 +352,6 @@
     uint8_t GetRouteCost(uint8_t aRouterIndex) const { return mRouteData[aRouterIndex] & kRouteCostMask; }
 
     /**
-     * This method sets the Route Cost value for a given Router index.
-     *
-     * @param[in]  aRouterIndex  The Router index.
-     * @param[in]  aRouteCost    The Route Cost value.
-     *
-     */
-    void SetRouteCost(uint8_t aRouterIndex, uint8_t aRouteCost)
-    {
-        mRouteData[aRouterIndex] = (mRouteData[aRouterIndex] & ~kRouteCostMask) | aRouteCost;
-    }
-
-    /**
      * This method returns the Link Quality In value for a given Router index.
      *
      * @param[in]  aRouterIndex  The Router index.
@@ -366,19 +365,6 @@
     }
 
     /**
-     * This method sets the Link Quality In value for a given Router index.
-     *
-     * @param[in]  aRouterIndex  The Router index.
-     * @param[in]  aLinkQuality  The Link Quality In value for a given Router index.
-     *
-     */
-    void SetLinkQualityIn(uint8_t aRouterIndex, LinkQuality aLinkQuality)
-    {
-        mRouteData[aRouterIndex] = (mRouteData[aRouterIndex] & ~kLinkQualityInMask) |
-                                   ((aLinkQuality << kLinkQualityInOffset) & kLinkQualityInMask);
-    }
-
-    /**
      * This method returns the Link Quality Out value for a given Router index.
      *
      * @param[in]  aRouterIndex  The Router index.
@@ -392,16 +378,19 @@
     }
 
     /**
-     * This method sets the Link Quality Out value for a given Router index.
+     * This method sets the Route Data (Link Quality In/Out and Route Cost) for a given Router index.
      *
-     * @param[in]  aRouterIndex  The Router index.
-     * @param[in]  aLinkQuality  The Link Quality Out value for a given Router index.
+     * @param[in]  aRouterIndex    The Router index.
+     * @param[in]  aLinkQualityIn  The Link Quality In value.
+     * @param[in]  aLinkQualityOut The Link Quality Out value.
+     * @param[in]  aRouteCost      The Route Cost value.
      *
      */
-    void SetLinkQualityOut(uint8_t aRouterIndex, LinkQuality aLinkQuality)
+    void SetRouteData(uint8_t aRouterIndex, LinkQuality aLinkQualityIn, LinkQuality aLinkQualityOut, uint8_t aRouteCost)
     {
-        mRouteData[aRouterIndex] = (mRouteData[aRouterIndex] & ~kLinkQualityOutMask) |
-                                   ((aLinkQuality << kLinkQualityOutOffset) & kLinkQualityOutMask);
+        mRouteData[aRouterIndex] = (((aLinkQualityIn << kLinkQualityInOffset) & kLinkQualityInMask) |
+                                    ((aLinkQualityOut << kLinkQualityOutOffset) & kLinkQualityOutMask) |
+                                    ((aRouteCost << kRouteCostOffset) & kRouteCostMask));
     }
 
 private:
@@ -488,6 +477,15 @@
     bool IsRouterIdSet(uint8_t aRouterId) const { return mRouterIdMask.Contains(aRouterId); }
 
     /**
+     * This method indicates whether the `RouteTlv` is a singleton, i.e., only one router is allocated.
+     *
+     * @retval TRUE   It is a singleton.
+     * @retval FALSE  It is not a singleton.
+     *
+     */
+    bool IsSingleton(void) const { return IsValid() && (mRouterIdMask.GetNumberOfAllocatedIds() <= 1); }
+
+    /**
      * This method sets the Router ID bit.
      *
      * @param[in]  aRouterId  The Router ID bit to set.
@@ -539,30 +537,6 @@
     }
 
     /**
-     * This method sets the Route Cost value for a given Router index.
-     *
-     * @param[in]  aRouterIndex  The Router index.
-     * @param[in]  aRouteCost    The Route Cost value.
-     *
-     */
-    void SetRouteCost(uint8_t aRouterIndex, uint8_t aRouteCost)
-    {
-        if (aRouterIndex & 1)
-        {
-            mRouteData[aRouterIndex + aRouterIndex / 2 + 1] = aRouteCost;
-        }
-        else
-        {
-            mRouteData[aRouterIndex + aRouterIndex / 2] =
-                (mRouteData[aRouterIndex + aRouterIndex / 2] & ~kRouteCostMask) |
-                ((aRouteCost >> kOddEntryOffset) & kRouteCostMask);
-            mRouteData[aRouterIndex + aRouterIndex / 2 + 1] = static_cast<uint8_t>(
-                (mRouteData[aRouterIndex + aRouterIndex / 2 + 1] & ~(kRouteCostMask << kOddEntryOffset)) |
-                ((aRouteCost & kRouteCostMask) << kOddEntryOffset));
-        }
-    }
-
-    /**
      * This method returns the Link Quality In value for a given Router index.
      *
      * @param[in]  aRouterIndex  The Router index.
@@ -570,26 +544,12 @@
      * @returns The Link Quality In value for a given Router index.
      *
      */
-    uint8_t GetLinkQualityIn(uint8_t aRouterIndex) const
+    LinkQuality GetLinkQualityIn(uint8_t aRouterIndex) const
     {
         int offset = ((aRouterIndex & 1) ? kOddEntryOffset : 0);
-        return (mRouteData[aRouterIndex + aRouterIndex / 2] & (kLinkQualityInMask >> offset)) >>
-               (kLinkQualityInOffset - offset);
-    }
-
-    /**
-     * This method sets the Link Quality In value for a given Router index.
-     *
-     * @param[in]  aRouterIndex  The Router index.
-     * @param[in]  aLinkQuality  The Link Quality In value for a given Router index.
-     *
-     */
-    void SetLinkQualityIn(uint8_t aRouterIndex, uint8_t aLinkQuality)
-    {
-        int offset = ((aRouterIndex & 1) ? kOddEntryOffset : 0);
-        mRouteData[aRouterIndex + aRouterIndex / 2] =
-            (mRouteData[aRouterIndex + aRouterIndex / 2] & ~(kLinkQualityInMask >> offset)) |
-            ((aLinkQuality << (kLinkQualityInOffset - offset)) & (kLinkQualityInMask >> offset));
+        return static_cast<LinkQuality>(
+            (mRouteData[aRouterIndex + aRouterIndex / 2] & (kLinkQualityInMask >> offset)) >>
+            (kLinkQualityInOffset - offset));
     }
 
     /**
@@ -609,18 +569,19 @@
     }
 
     /**
-     * This method sets the Link Quality Out value for a given Router index.
+     * This method sets the Route Data (Link Quality In/Out and Route Cost) for a given Router index.
      *
-     * @param[in]  aRouterIndex  The Router index.
-     * @param[in]  aLinkQuality  The Link Quality Out value for a given Router index.
+     * @param[in]  aRouterIndex    The Router index.
+     * @param[in]  aLinkQualityIn  The Link Quality In value.
+     * @param[in]  aLinkQualityOut The Link Quality Out value.
+     * @param[in]  aRouteCost      The Route Cost value.
      *
      */
-    void SetLinkQualityOut(uint8_t aRouterIndex, LinkQuality aLinkQuality)
+    void SetRouteData(uint8_t aRouterIndex, LinkQuality aLinkQualityIn, LinkQuality aLinkQualityOut, uint8_t aRouteCost)
     {
-        int offset = ((aRouterIndex & 1) ? kOddEntryOffset : 0);
-        mRouteData[aRouterIndex + aRouterIndex / 2] =
-            (mRouteData[aRouterIndex + aRouterIndex / 2] & ~(kLinkQualityOutMask >> offset)) |
-            ((aLinkQuality << (kLinkQualityOutOffset - offset)) & (kLinkQualityOutMask >> offset));
+        SetLinkQualityIn(aRouterIndex, aLinkQualityIn);
+        SetLinkQualityOut(aRouterIndex, aLinkQualityOut);
+        SetRouteCost(aRouterIndex, aRouteCost);
     }
 
 private:
@@ -632,6 +593,39 @@
     static constexpr uint8_t kRouteCostMask        = 0xf << kRouteCostOffset;
     static constexpr uint8_t kOddEntryOffset       = 4;
 
+    void SetRouteCost(uint8_t aRouterIndex, uint8_t aRouteCost)
+    {
+        if (aRouterIndex & 1)
+        {
+            mRouteData[aRouterIndex + aRouterIndex / 2 + 1] = aRouteCost;
+        }
+        else
+        {
+            mRouteData[aRouterIndex + aRouterIndex / 2] =
+                (mRouteData[aRouterIndex + aRouterIndex / 2] & ~kRouteCostMask) |
+                ((aRouteCost >> kOddEntryOffset) & kRouteCostMask);
+            mRouteData[aRouterIndex + aRouterIndex / 2 + 1] = static_cast<uint8_t>(
+                (mRouteData[aRouterIndex + aRouterIndex / 2 + 1] & ~(kRouteCostMask << kOddEntryOffset)) |
+                ((aRouteCost & kRouteCostMask) << kOddEntryOffset));
+        }
+    }
+
+    void SetLinkQualityIn(uint8_t aRouterIndex, uint8_t aLinkQuality)
+    {
+        int offset = ((aRouterIndex & 1) ? kOddEntryOffset : 0);
+        mRouteData[aRouterIndex + aRouterIndex / 2] =
+            (mRouteData[aRouterIndex + aRouterIndex / 2] & ~(kLinkQualityInMask >> offset)) |
+            ((aLinkQuality << (kLinkQualityInOffset - offset)) & (kLinkQualityInMask >> offset));
+    }
+
+    void SetLinkQualityOut(uint8_t aRouterIndex, LinkQuality aLinkQuality)
+    {
+        int offset = ((aRouterIndex & 1) ? kOddEntryOffset : 0);
+        mRouteData[aRouterIndex + aRouterIndex / 2] =
+            (mRouteData[aRouterIndex + aRouterIndex / 2] & ~(kLinkQualityOutMask >> offset)) |
+            ((aLinkQuality << (kLinkQualityOutOffset - offset)) & (kLinkQualityOutMask >> offset));
+    }
+
     uint8_t     mRouterIdSequence;
     RouterIdSet mRouterIdMask;
     // Since we do hold 12 (compressible to 11) bits of data per router, each entry occupies 1.5 bytes,
@@ -783,10 +777,7 @@
      * @returns The Parent Priority value.
      *
      */
-    int8_t GetParentPriority(void) const
-    {
-        return (static_cast<int8_t>(mParentPriority & kParentPriorityMask)) >> kParentPriorityOffset;
-    }
+    int8_t GetParentPriority(void) const;
 
     /**
      * This method sets the Parent Priority value.
@@ -794,10 +785,7 @@
      * @param[in] aParentPriority  The Parent Priority value.
      *
      */
-    void SetParentPriority(int8_t aParentPriority)
-    {
-        mParentPriority = (static_cast<uint8_t>(aParentPriority) << kParentPriorityOffset) & kParentPriorityMask;
-    }
+    void SetParentPriority(int8_t aParentPriority);
 
     /**
      * This method returns the Link Quality 3 value.
@@ -848,6 +836,17 @@
     void SetLinkQuality1(uint8_t aLinkQuality) { mLinkQuality1 = aLinkQuality; }
 
     /**
+     * This method increments the Link Quality N field in TLV for a given Link Quality N (1,2,3).
+     *
+     * The Link Quality N field specifies the number of neighboring router devices with which the sender shares a link
+     * of quality N.
+     *
+     * @param[in] aLinkQuality  The Link Quality N (1,2,3) field to update.
+     *
+     */
+    void IncrementLinkQuality(LinkQuality aLinkQuality);
+
+    /**
      * This method sets the Active Routers value.
      *
      * @returns The Active Routers value.
@@ -946,10 +945,10 @@
     void SetSedDatagramCount(uint8_t aSedDatagramCount) { mSedDatagramCount = aSedDatagramCount; }
 
 private:
-    static constexpr uint8_t kParentPriorityOffset = 6;
-    static constexpr uint8_t kParentPriorityMask   = 3 << kParentPriorityOffset;
+    static constexpr uint8_t kFlagsParentPriorityOffset = 6;
+    static constexpr uint8_t kFlagsParentPriorityMask   = (3 << kFlagsParentPriorityOffset);
 
-    uint8_t  mParentPriority;
+    uint8_t  mFlags;
     uint8_t  mLinkQuality3;
     uint8_t  mLinkQuality2;
     uint8_t  mLinkQuality1;
@@ -976,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.
@@ -1230,7 +1190,7 @@
      * @retval FALSE  If the TLV does not appear to be well-formed.
      *
      */
-    bool IsValid(void) const { return GetLength() == sizeof(*this) - sizeof(Tlv); }
+    bool IsValid(void) const { return GetLength() >= sizeof(*this) - sizeof(Tlv); }
 
     /**
      * This method returns the Channel Page value.
@@ -1291,12 +1251,21 @@
     }
 
     /**
+     * This method indicates whether or not the TLV appears to be well-formed.
+     *
+     * @retval TRUE   If the TLV appears to be well-formed.
+     * @retval FALSE  If the TLV does not appear to be well-formed.
+     *
+     */
+    bool IsValid(void) const { return GetLength() >= sizeof(*this) - sizeof(Tlv); }
+
+    /**
      * This method returns the CSL Clock Accuracy value.
      *
      * @returns The CSL Clock Accuracy value.
      *
      */
-    uint8_t GetCslClockAccuracy(void) { return mCslClockAccuracy; }
+    uint8_t GetCslClockAccuracy(void) const { return mCslClockAccuracy; }
 
     /**
      * This method sets the CSL Clock Accuracy value.
@@ -1307,12 +1276,12 @@
     void SetCslClockAccuracy(uint8_t aCslClockAccuracy) { mCslClockAccuracy = aCslClockAccuracy; }
 
     /**
-     * This method returns the Clock Accuracy value.
+     * This method returns the Clock Uncertainty value.
      *
-     * @returns The Clock Accuracy value.
+     * @returns The Clock Uncertainty value.
      *
      */
-    uint8_t GetCslUncertainty(void) { return mCslUncertainty; }
+    uint8_t GetCslUncertainty(void) const { return mCslUncertainty; }
 
     /**
      * This method sets the CSL Uncertainty value.
diff --git a/src/core/thread/mle_types.cpp b/src/core/thread/mle_types.cpp
index 28233ba..ca76165 100644
--- a/src/core/thread/mle_types.cpp
+++ b/src/core/thread/mle_types.cpp
@@ -33,11 +33,15 @@
 
 #include "mle_types.hpp"
 
+#include "common/array.hpp"
 #include "common/code_utils.hpp"
 
 namespace ot {
 namespace Mle {
 
+//---------------------------------------------------------------------------------------------------------------------
+// DeviceMode
+
 void DeviceMode::Get(ModeConfig &aModeConfig) const
 {
     aModeConfig.mRxOnWhenIdle = IsRxOnWhenIdle();
@@ -63,5 +67,109 @@
     return string;
 }
 
+//---------------------------------------------------------------------------------------------------------------------
+// DeviceProperties
+
+#if OPENTHREAD_FTD
+
+DeviceProperties::DeviceProperties(void)
+{
+    Clear();
+
+    mPowerSupply            = OPENTHREAD_CONFIG_DEVICE_POWER_SUPPLY;
+    mLeaderWeightAdjustment = kDefaultAdjustment;
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    mIsBorderRouter = true;
+#endif
+}
+
+void DeviceProperties::ClampWeightAdjustment(void)
+{
+    mLeaderWeightAdjustment = Clamp(mLeaderWeightAdjustment, kMinAdjustment, kMaxAdjustment);
+}
+
+uint8_t DeviceProperties::CalculateLeaderWeight(void) const
+{
+    static const int8_t kPowerSupplyIncs[] = {
+        kPowerBatteryInc,          // (0) kPowerSupplyBattery
+        kPowerExternalInc,         // (1) kPowerSupplyExternal
+        kPowerExternalStableInc,   // (2) kPowerSupplyExternalStable
+        kPowerExternalUnstableInc, // (3) kPowerSupplyExternalUnstable
+    };
+
+    static_assert(0 == kPowerSupplyBattery, "kPowerSupplyBattery value is incorrect");
+    static_assert(1 == kPowerSupplyExternal, "kPowerSupplyExternal value is incorrect");
+    static_assert(2 == kPowerSupplyExternalStable, "kPowerSupplyExternalStable value is incorrect");
+    static_assert(3 == kPowerSupplyExternalUnstable, "kPowerSupplyExternalUnstable value is incorrect");
+
+    uint8_t     weight      = kBaseWeight;
+    PowerSupply powerSupply = MapEnum(mPowerSupply);
+
+    if (mIsBorderRouter)
+    {
+        weight += (mSupportsCcm ? kCcmBorderRouterInc : kBorderRouterInc);
+    }
+
+    if (powerSupply < GetArrayLength(kPowerSupplyIncs))
+    {
+        weight += kPowerSupplyIncs[powerSupply];
+    }
+
+    if (mIsUnstable)
+    {
+        switch (powerSupply)
+        {
+        case kPowerSupplyBattery:
+        case kPowerSupplyExternalUnstable:
+            break;
+
+        default:
+            weight += kIsUnstableInc;
+        }
+    }
+
+    weight += mLeaderWeightAdjustment;
+
+    return weight;
+}
+
+#endif // OPENTHREAD_FTD
+
+//---------------------------------------------------------------------------------------------------------------------
+// RouterIdSet
+
+uint8_t RouterIdSet::GetNumberOfAllocatedIds(void) const
+{
+    uint8_t count = 0;
+
+    for (uint8_t byte : mRouterIdSet)
+    {
+        count += CountBitsInMask(byte);
+    }
+
+    return count;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+const char *RoleToString(DeviceRole aRole)
+{
+    static const char *const kRoleStrings[] = {
+        "disabled", // (0) kRoleDisabled
+        "detached", // (1) kRoleDetached
+        "child",    // (2) kRoleChild
+        "router",   // (3) kRoleRouter
+        "leader",   // (4) kRoleLeader
+    };
+
+    static_assert(kRoleDisabled == 0, "kRoleDisabled value is incorrect");
+    static_assert(kRoleDetached == 1, "kRoleDetached value is incorrect");
+    static_assert(kRoleChild == 2, "kRoleChild value is incorrect");
+    static_assert(kRoleRouter == 3, "kRoleRouter value is incorrect");
+    static_assert(kRoleLeader == 4, "kRoleLeader value is incorrect");
+
+    return (aRole < GetArrayLength(kRoleStrings)) ? kRoleStrings[aRole] : "invalid";
+}
+
 } // namespace Mle
 } // namespace ot
diff --git a/src/core/thread/mle_types.hpp b/src/core/thread/mle_types.hpp
index fbc92a6..286e63a 100644
--- a/src/core/thread/mle_types.hpp
+++ b/src/core/thread/mle_types.hpp
@@ -41,6 +41,9 @@
 #include <string.h>
 
 #include <openthread/thread.h>
+#if OPENTHREAD_FTD
+#include <openthread/thread_ftd.h>
+#endif
 
 #include "common/as_core_type.hpp"
 #include "common/clearable.hpp"
@@ -77,8 +80,7 @@
 constexpr uint8_t  kMaxServiceAlocs      = OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_MAX_ALOCS;
 #endif
 
-constexpr uint8_t  kThreadVersion = OPENTHREAD_CONFIG_THREAD_VERSION; ///< Thread Version
-constexpr uint16_t kUdpPort       = 19788;                            ///< MLE UDP Port
+constexpr uint16_t kUdpPort = 19788; ///< MLE UDP Port
 
 /*
  * MLE Protocol delays and timeouts.
@@ -87,6 +89,7 @@
 constexpr uint32_t kParentRequestRouterTimeout     = 750;  ///< Router Parent Request timeout (in msec)
 constexpr uint32_t kParentRequestDuplicateMargin   = 50;   ///< Margin for duplicate parent request
 constexpr uint32_t kParentRequestReedTimeout       = 1250; ///< Router and REEDs Parent Request timeout (in msec)
+constexpr uint32_t kChildIdResponseTimeout         = 1250; ///< Wait time to receive Child ID Response (in msec)
 constexpr uint32_t kAttachStartJitter              = 50;   ///< Max jitter time added to start of attach (in msec)
 constexpr uint32_t kAnnounceProcessTimeout         = 250;  ///< Delay after Announce rx before channel/pan-id change
 constexpr uint32_t kAnnounceTimeout                = 1400; ///< Total timeout for sending Announce messages (in msec)
@@ -97,10 +100,16 @@
 constexpr uint32_t kChildUpdateRequestPendingDelay = 100;  ///< Delay for aggregating Child Update Req (in msec)
 constexpr uint8_t  kMaxTransmissionCount           = 3;    ///< Max number of times an MLE message may be transmitted
 constexpr uint32_t kMaxResponseDelay               = 1000; ///< Max response delay for a multicast request (in msec)
-constexpr uint32_t kMaxChildIdRequestTimeout       = 5000; ///< Max delay to rx a Child ID Request (in msec)
-constexpr uint32_t kMaxChildUpdateResponseTimeout  = 2000; ///< Max delay to rx a Child Update Response (in msec)
-constexpr uint32_t kMaxLinkRequestTimeout          = 2000; ///< Max delay to rx a Link Accept
+constexpr uint32_t kChildIdRequestTimeout          = 5000; ///< Max delay to rx a Child ID Request (in msec)
+constexpr uint32_t kLinkRequestTimeout             = 2000; ///< Max delay to rx a Link Accept
 constexpr uint8_t  kMulticastLinkRequestDelay      = 5;    ///< Max delay for sending a mcast Link Request (in sec)
+constexpr uint8_t kMaxCriticalTransmissionCount = 6; ///< Max number of times an critical MLE message may be transmitted
+
+constexpr uint32_t kMulticastTransmissionDelay = 5000; ///< Delay for retransmitting a multicast packet (in msec)
+constexpr uint32_t kMulticastTransmissionDelayMin =
+    kMulticastTransmissionDelay * 9 / 10; ///< Min delay for retransmitting a multicast packet (in msec)
+constexpr uint32_t kMulticastTransmissionDelayMax =
+    kMulticastTransmissionDelay * 11 / 10; ///< Max delay for retransmitting a multicast packet (in msec)
 
 constexpr uint32_t kMinTimeoutKeepAlive = (((kMaxChildKeepAliveAttempts + 1) * kUnicastRetransmissionDelay) / 1000);
 constexpr uint32_t kMinPollPeriod       = OPENTHREAD_CONFIG_MAC_MINIMUM_POLL_PERIOD;
@@ -121,8 +130,8 @@
 constexpr uint8_t kRouterIdOffset   = 10; ///< Bit offset of Router ID in RLOC16
 constexpr uint8_t kRlocPrefixLength = 14; ///< Prefix length of RLOC in bytes
 
-constexpr uint8_t kMinChallengeSize = 4; ///< Minimum Challenge size in bytes.
-constexpr uint8_t kMaxChallengeSize = 8; ///< Maximum Challenge size in bytes.
+constexpr uint16_t kMinChallengeSize = 4; ///< Minimum Challenge size in bytes.
+constexpr uint16_t kMaxChallengeSize = 8; ///< Maximum Challenge size in bytes.
 
 /*
  * Routing Protocol Constants
@@ -162,6 +171,8 @@
 constexpr uint8_t kRouterDowngradeThreshold = 23;
 constexpr uint8_t kRouterUpgradeThreshold   = 16;
 
+constexpr uint16_t kInvalidRloc16 = Mac::kShortAddrInvalid; ///< Invalid RLOC16.
+
 /**
  * Threshold to accept a router upgrade request with reason `kBorderRouterRequest` (number of BRs acting as router in
  * Network Data).
@@ -173,7 +184,6 @@
 constexpr uint32_t kReedAdvertiseInterval    = 570; ///< (in sec)
 constexpr uint32_t kReedAdvertiseJitter      = 60;  ///< (in sec)
 
-constexpr uint8_t  kLeaderWeight             = 64;                                          ///< Default leader weight
 constexpr uint32_t kMleEndDeviceTimeout      = OPENTHREAD_CONFIG_MLE_CHILD_TIMEOUT_DEFAULT; ///< (in sec)
 constexpr uint8_t  kMeshLocalPrefixContextId = 0; ///< 0 is reserved for Mesh Local Prefix
 
@@ -182,14 +192,6 @@
 constexpr int8_t kParentPriorityLow         = -1; ///< Parent Priority Low
 constexpr int8_t kParentPriorityUnspecified = -2; ///< Parent Priority Unspecified
 
-constexpr uint8_t kLinkQuality3LinkCost = 1;             ///< Link Cost for Link Quality 3
-constexpr uint8_t kLinkQuality2LinkCost = 2;             ///< Link Cost for Link Quality 2
-constexpr uint8_t kLinkQuality1LinkCost = 4;             ///< Link Cost for Link Quality 1
-constexpr uint8_t kLinkQuality0LinkCost = kMaxRouteCost; ///< Link Cost for Link Quality 0
-
-constexpr uint8_t kMplChildDataMessageTimerExpirations  = 0; ///< Number of MPL retransmissions for Children.
-constexpr uint8_t kMplRouterDataMessageTimerExpirations = 2; ///< Number of MPL retransmissions for Routers.
-
 /**
  * This type represents a Thread device role.
  *
@@ -236,7 +238,7 @@
  * Backbone Router / DUA / MLR constants
  *
  */
-constexpr uint16_t kRegistrationDelayDefault         = 1200;              ///< In seconds.
+constexpr uint16_t kRegistrationDelayDefault         = 5;                 ///< In seconds.
 constexpr uint32_t kMlrTimeoutDefault                = 3600;              ///< In seconds.
 constexpr uint32_t kMlrTimeoutMin                    = 300;               ///< In seconds.
 constexpr uint32_t kMlrTimeoutMax                    = 0x7fffffff / 1000; ///< In seconds (about 24 days).
@@ -435,6 +437,67 @@
     uint8_t mMode;
 };
 
+#if OPENTHREAD_FTD
+/**
+ * This class represents device properties.
+ *
+ * The device properties are used for calculating the local leader weight on the device.
+ *
+ */
+class DeviceProperties : public otDeviceProperties, public Clearable<DeviceProperties>
+{
+public:
+    /**
+     * This enumeration represents the device's power supply property.
+     *
+     */
+    enum PowerSupply : uint8_t
+    {
+        kPowerSupplyBattery          = OT_POWER_SUPPLY_BATTERY,           ///< Battery powered.
+        kPowerSupplyExternal         = OT_POWER_SUPPLY_EXTERNAL,          ///< External powered.
+        kPowerSupplyExternalStable   = OT_POWER_SUPPLY_EXTERNAL_STABLE,   ///< Stable external power with backup.
+        kPowerSupplyExternalUnstable = OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, ///< Unstable external power.
+    };
+
+    /**
+     * This constructor initializes `DeviceProperties` with default values.
+     *
+     */
+    DeviceProperties(void);
+
+    /**
+     * This method clamps the `mLeaderWeightAdjustment` value to the valid range.
+     *
+     */
+    void ClampWeightAdjustment(void);
+
+    /**
+     * This method calculates the leader weight based on the device properties.
+     *
+     * @returns The calculated leader weight.
+     *
+     */
+    uint8_t CalculateLeaderWeight(void) const;
+
+private:
+    static constexpr int8_t  kDefaultAdjustment        = OPENTHREAD_CONFIG_MLE_DEFAULT_LEADER_WEIGHT_ADJUSTMENT;
+    static constexpr uint8_t kBaseWeight               = 64;
+    static constexpr int8_t  kBorderRouterInc          = +1;
+    static constexpr int8_t  kCcmBorderRouterInc       = +8;
+    static constexpr int8_t  kIsUnstableInc            = -4;
+    static constexpr int8_t  kPowerBatteryInc          = -8;
+    static constexpr int8_t  kPowerExternalInc         = 0;
+    static constexpr int8_t  kPowerExternalStableInc   = +4;
+    static constexpr int8_t  kPowerExternalUnstableInc = -4;
+    static constexpr int8_t  kMinAdjustment            = -16;
+    static constexpr int8_t  kMaxAdjustment            = +16;
+
+    static_assert(kDefaultAdjustment >= kMinAdjustment, "Invalid default weight adjustment");
+    static_assert(kDefaultAdjustment <= kMaxAdjustment, "Invalid default weight adjustment");
+};
+
+#endif // OPENTHREAD_FTD
+
 /**
  * This class represents the Thread Leader Data.
  *
@@ -539,7 +602,7 @@
      * @retval FALSE  If the Router ID bit is not set.
      *
      */
-    bool Contains(uint8_t aRouterId) const { return (mRouterIdSet[aRouterId / 8] & (0x80 >> (aRouterId % 8))) != 0; }
+    bool Contains(uint8_t aRouterId) const { return (mRouterIdSet[aRouterId / 8] & MaskFor(aRouterId)) != 0; }
 
     /**
      * This method sets a given Router ID.
@@ -547,7 +610,7 @@
      * @param[in]  aRouterId  The Router ID to set.
      *
      */
-    void Add(uint8_t aRouterId) { mRouterIdSet[aRouterId / 8] |= 0x80 >> (aRouterId % 8); }
+    void Add(uint8_t aRouterId) { mRouterIdSet[aRouterId / 8] |= MaskFor(aRouterId); }
 
     /**
      * This method removes a given Router ID.
@@ -555,9 +618,19 @@
      * @param[in]  aRouterId  The Router ID to remove.
      *
      */
-    void Remove(uint8_t aRouterId) { mRouterIdSet[aRouterId / 8] &= ~(0x80 >> (aRouterId % 8)); }
+    void Remove(uint8_t aRouterId) { mRouterIdSet[aRouterId / 8] &= ~MaskFor(aRouterId); }
+
+    /**
+     * This method calculates the number of allocated Router IDs in the set.
+     *
+     * @returns The number of allocated Router IDs in the set.
+     *
+     */
+    uint8_t GetNumberOfAllocatedIds(void) const;
 
 private:
+    static uint8_t MaskFor(uint8_t aRouterId) { return (0x80 >> (aRouterId % 8)); }
+
     uint8_t mRouterIdSet[BitVectorBytes(Mle::kMaxRouterId + 1)];
 } OT_TOOL_PACKED_END;
 
@@ -574,6 +647,113 @@
 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.
+ *
+ * @returns The Child ID portion of an RLOC16.
+ *
+ */
+inline uint16_t ChildIdFromRloc16(uint16_t aRloc16) { return aRloc16 & kMaxChildId; }
+
+/**
+ * This function derives the Router ID portion from a given RLOC16.
+ *
+ * @param[in]  aRloc16  The RLOC16 value.
+ *
+ * @returns The Router ID portion of an RLOC16.
+ *
+ */
+inline uint8_t RouterIdFromRloc16(uint16_t aRloc16) { return aRloc16 >> kRouterIdOffset; }
+
+/**
+ * This function returns whether the two RLOC16 have the same Router ID.
+ *
+ * @param[in]  aRloc16A  The first RLOC16 value.
+ * @param[in]  aRloc16B  The second RLOC16 value.
+ *
+ * @returns true if the two RLOC16 have the same Router ID, false otherwise.
+ *
+ */
+inline bool RouterIdMatch(uint16_t aRloc16A, uint16_t aRloc16B)
+{
+    return RouterIdFromRloc16(aRloc16A) == RouterIdFromRloc16(aRloc16B);
+}
+
+/**
+ * This function returns the Service ID corresponding to a Service ALOC16.
+ *
+ * @param[in]  aAloc16  The Service ALOC16 value.
+ *
+ * @returns The Service ID corresponding to given ALOC16.
+ *
+ */
+inline uint8_t ServiceIdFromAloc(uint16_t aAloc16) { return static_cast<uint8_t>(aAloc16 - kAloc16ServiceStart); }
+
+/**
+ * This function returns the Service ALOC16 corresponding to a Service ID.
+ *
+ * @param[in]  aServiceId  The Service ID value.
+ *
+ * @returns The Service ALOC16 corresponding to given ID.
+ *
+ */
+inline uint16_t ServiceAlocFromId(uint8_t aServiceId)
+{
+    return static_cast<uint16_t>(aServiceId + kAloc16ServiceStart);
+}
+
+/**
+ * This function returns the Commissioner Aloc corresponding to a Commissioner Session ID.
+ *
+ * @param[in]  aSessionId   The Commissioner Session ID value.
+ *
+ * @returns The Commissioner ALOC16 corresponding to given ID.
+ *
+ */
+inline uint16_t CommissionerAloc16FromId(uint16_t aSessionId)
+{
+    return static_cast<uint16_t>((aSessionId & kAloc16CommissionerMask) + kAloc16CommissionerStart);
+}
+
+/**
+ * This function derives RLOC16 from a given Router ID.
+ *
+ * @param[in]  aRouterId  The Router ID value.
+ *
+ * @returns The RLOC16 corresponding to the given Router ID.
+ *
+ */
+inline uint16_t Rloc16FromRouterId(uint8_t aRouterId) { return static_cast<uint16_t>(aRouterId << kRouterIdOffset); }
+
+/**
+ * This function indicates whether or not @p aRloc16 refers to an active router.
+ *
+ * @param[in]  aRloc16  The RLOC16 value.
+ *
+ * @retval TRUE   If @p aRloc16 refers to an active router.
+ * @retval FALSE  If @p aRloc16 does not refer to an active router.
+ *
+ */
+inline bool IsActiveRouter(uint16_t aRloc16) { return ChildIdFromRloc16(aRloc16) == 0; }
+
+/**
+ * This function converts a device role into a human-readable string.
+ *
+ * @param[in] aRole  The device role to convert.
+ *
+ * @returns The string representation of @p aRole.
+ *
+ */
+const char *RoleToString(DeviceRole aRole);
+
+/**
  * @}
  *
  */
@@ -582,6 +762,10 @@
 
 DefineCoreType(otLeaderData, Mle::LeaderData);
 DefineMapEnum(otDeviceRole, Mle::DeviceRole);
+#if OPENTHREAD_FTD
+DefineCoreType(otDeviceProperties, Mle::DeviceProperties);
+DefineMapEnum(otPowerSupply, Mle::DeviceProperties::PowerSupply);
+#endif
 
 } // namespace ot
 
diff --git a/src/core/thread/mlr_manager.cpp b/src/core/thread/mlr_manager.cpp
index 98272fe..d8e8a03 100644
--- a/src/core/thread/mlr_manager.cpp
+++ b/src/core/thread/mlr_manager.cpp
@@ -51,10 +51,6 @@
 
 MlrManager::MlrManager(Instance &aInstance)
     : InstanceLocator(aInstance)
-#if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE) && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE
-    , mRegisterMulticastListenersCallback(nullptr)
-    , mRegisterMulticastListenersContext(nullptr)
-#endif
     , mReregistrationDelay(0)
     , mSendDelay(0)
     , mMlrPending(false)
@@ -80,8 +76,8 @@
     }
 }
 
-void MlrManager::HandleBackboneRouterPrimaryUpdate(BackboneRouter::Leader::State               aState,
-                                                   const BackboneRouter::BackboneRouterConfig &aConfig)
+void MlrManager::HandleBackboneRouterPrimaryUpdate(BackboneRouter::Leader::State aState,
+                                                   const BackboneRouter::Config &aConfig)
 {
     OT_UNUSED_VARIABLE(aConfig);
 
@@ -150,7 +146,7 @@
     return ret;
 }
 
-void MlrManager::UpdateProxiedSubscriptions(Child &             aChild,
+void MlrManager::UpdateProxiedSubscriptions(Child              &aChild,
                                             const Ip6::Address *aOldMlrRegisteredAddresses,
                                             uint16_t            aOldMlrRegisteredAddressNum)
 {
@@ -293,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())
@@ -317,11 +313,11 @@
 }
 
 #if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE) && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE
-Error MlrManager::RegisterMulticastListeners(const otIp6Address *                    aAddresses,
+Error MlrManager::RegisterMulticastListeners(const otIp6Address                     *aAddresses,
                                              uint8_t                                 aAddressNum,
-                                             const uint32_t *                        aTimeout,
+                                             const uint32_t                         *aTimeout,
                                              otIp6RegisterMulticastListenersCallback aCallback,
-                                             void *                                  aContext)
+                                             void                                   *aContext)
 {
     Error error;
 
@@ -343,16 +339,15 @@
     SuccessOrExit(error = SendMulticastListenerRegistrationMessage(
                       aAddresses, aAddressNum, aTimeout, &MlrManager::HandleRegisterMulticastListenersResponse, this));
 
-    mRegisterMulticastListenersPending  = true;
-    mRegisterMulticastListenersCallback = aCallback;
-    mRegisterMulticastListenersContext  = aContext;
+    mRegisterMulticastListenersPending = true;
+    mRegisterMulticastListenersCallback.Set(aCallback, aContext);
 
 exit:
     return error;
 }
 
-void MlrManager::HandleRegisterMulticastListenersResponse(void *               aContext,
-                                                          otMessage *          aMessage,
+void MlrManager::HandleRegisterMulticastListenersResponse(void                *aContext,
+                                                          otMessage           *aMessage,
                                                           const otMessageInfo *aMessageInfo,
                                                           Error                aResult)
 {
@@ -360,51 +355,46 @@
                                                                                   AsCoreTypePtr(aMessageInfo), aResult);
 }
 
-void MlrManager::HandleRegisterMulticastListenersResponse(otMessage *          aMessage,
+void MlrManager::HandleRegisterMulticastListenersResponse(otMessage           *aMessage,
                                                           const otMessageInfo *aMessageInfo,
                                                           Error                aResult)
 {
     OT_UNUSED_VARIABLE(aMessageInfo);
 
-    uint8_t                                 status;
-    Error                                   error;
-    Ip6::Address                            failedAddresses[Ip6AddressesTlv::kMaxAddresses];
-    uint8_t                                 failedAddressNum = 0;
-    otIp6RegisterMulticastListenersCallback callback         = mRegisterMulticastListenersCallback;
-    void *                                  context          = mRegisterMulticastListenersContext;
+    uint8_t                                           status;
+    Error                                             error;
+    Ip6::Address                                      failedAddresses[Ip6AddressesTlv::kMaxAddresses];
+    uint8_t                                           failedAddressNum = 0;
+    Callback<otIp6RegisterMulticastListenersCallback> callbackCopy     = mRegisterMulticastListenersCallback;
 
-    mRegisterMulticastListenersPending  = false;
-    mRegisterMulticastListenersCallback = nullptr;
-    mRegisterMulticastListenersContext  = nullptr;
+    mRegisterMulticastListenersPending = false;
+    mRegisterMulticastListenersCallback.Clear();
 
     error = ParseMulticastListenerRegistrationResponse(aResult, AsCoapMessagePtr(aMessage), status, failedAddresses,
                                                        failedAddressNum);
 
-    if (callback != nullptr)
-    {
-        callback(context, error, status, failedAddresses, failedAddressNum);
-    }
+    callbackCopy.InvokeIfSet(error, status, failedAddresses, failedAddressNum);
 }
 
 #endif // (OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE) && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE
 
-Error MlrManager::SendMulticastListenerRegistrationMessage(const otIp6Address *  aAddresses,
+Error MlrManager::SendMulticastListenerRegistrationMessage(const otIp6Address   *aAddresses,
                                                            uint8_t               aAddressNum,
-                                                           const uint32_t *      aTimeout,
+                                                           const uint32_t       *aTimeout,
                                                            Coap::ResponseHandler aResponseHandler,
-                                                           void *                aResponseContext)
+                                                           void                 *aResponseContext)
 {
     OT_UNUSED_VARIABLE(aTimeout);
 
     Error            error   = kErrorNone;
-    Mle::MleRouter & mle     = Get<Mle::MleRouter>();
-    Coap::Message *  message = nullptr;
+    Mle::MleRouter  &mle     = Get<Mle::MleRouter>();
+    Coap::Message   *message = nullptr;
     Tmf::MessageInfo messageInfo(GetInstance());
     Ip6AddressesTlv  addressesTlv;
 
     VerifyOrExit(Get<BackboneRouter::Leader>().HasPrimary(), error = kErrorInvalidState);
 
-    message = Get<Tmf::Agent>().NewConfirmablePostMessage(UriPath::kMlr);
+    message = Get<Tmf::Agent>().NewConfirmablePostMessage(kUriMlr);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     addressesTlv.Init();
@@ -452,8 +442,8 @@
     return error;
 }
 
-void MlrManager::HandleMulticastListenerRegistrationResponse(void *               aContext,
-                                                             otMessage *          aMessage,
+void MlrManager::HandleMulticastListenerRegistrationResponse(void                *aContext,
+                                                             otMessage           *aMessage,
                                                              const otMessageInfo *aMessageInfo,
                                                              Error                aResult)
 {
@@ -461,7 +451,7 @@
         AsCoapMessagePtr(aMessage), AsCoreTypePtr(aMessageInfo), aResult);
 }
 
-void MlrManager::HandleMulticastListenerRegistrationResponse(Coap::Message *         aMessage,
+void MlrManager::HandleMulticastListenerRegistrationResponse(Coap::Message          *aMessage,
                                                              const Ip6::MessageInfo *aMessageInfo,
                                                              Error                   aResult)
 {
@@ -484,7 +474,7 @@
     }
     else
     {
-        otBackboneRouterConfig config;
+        BackboneRouter::Config config;
         uint16_t               reregDelay;
 
         // The Device has just attempted a Multicast Listener Registration which failed, and it retries the same
@@ -502,9 +492,9 @@
 
 Error MlrManager::ParseMulticastListenerRegistrationResponse(Error          aResult,
                                                              Coap::Message *aMessage,
-                                                             uint8_t &      aStatus,
-                                                             Ip6::Address * aFailedAddresses,
-                                                             uint8_t &      aFailedAddressNum)
+                                                             uint8_t       &aStatus,
+                                                             Ip6::Address  *aFailedAddresses,
+                                                             uint8_t       &aFailedAddressNum)
 {
     Error    error;
     uint16_t addressesOffset, addressesLength;
@@ -642,9 +632,9 @@
     }
     else
     {
-        BackboneRouter::BackboneRouterConfig config;
-        uint32_t                             reregDelay;
-        uint32_t                             effectiveMlrTimeout;
+        BackboneRouter::Config config;
+        uint32_t               reregDelay;
+        uint32_t               effectiveMlrTimeout;
 
         IgnoreError(Get<BackboneRouter::Leader>().GetConfig(config));
 
@@ -658,8 +648,7 @@
         {
             // Calculate renewing period according to Thread Spec. 5.24.2.3.2
             // The random time t SHOULD be chosen such that (0.5* MLR-Timeout) < t < (MLR-Timeout – 9 seconds).
-            effectiveMlrTimeout = config.mMlrTimeout > Mle::kMlrTimeoutMin ? config.mMlrTimeout
-                                                                           : static_cast<uint32_t>(Mle::kMlrTimeoutMin);
+            effectiveMlrTimeout = Max(config.mMlrTimeout, Mle::kMlrTimeoutMin);
             reregDelay = Random::NonCrypto::GetUint32InRange((effectiveMlrTimeout >> 1u) + 1, effectiveMlrTimeout - 9);
         }
 
@@ -672,7 +661,7 @@
     UpdateTimeTickerRegistration();
 
     LogDebg("MlrManager::UpdateReregistrationDelay: rereg=%d, needSendMlr=%d, ReregDelay=%lu", aRereg, needSendMlr,
-            mReregistrationDelay);
+            ToUlong(mReregistrationDelay));
 }
 
 void MlrManager::LogMulticastAddresses(void)
@@ -702,7 +691,7 @@
 }
 
 void MlrManager::AppendToUniqueAddressList(Ip6::Address (&aAddresses)[Ip6AddressesTlv::kMaxAddresses],
-                                           uint8_t &           aAddressNum,
+                                           uint8_t            &aAddressNum,
                                            const Ip6::Address &aAddress)
 {
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
diff --git a/src/core/thread/mlr_manager.hpp b/src/core/thread/mlr_manager.hpp
index 6eda998..b78e0fb 100644
--- a/src/core/thread/mlr_manager.hpp
+++ b/src/core/thread/mlr_manager.hpp
@@ -44,6 +44,7 @@
 
 #include "backbone_router/bbr_leader.hpp"
 #include "coap/coap_message.hpp"
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "common/notifier.hpp"
@@ -94,8 +95,7 @@
      * @param[in]  aConfig  The Primary Backbone Router service.
      *
      */
-    void HandleBackboneRouterPrimaryUpdate(BackboneRouter::Leader::State               aState,
-                                           const BackboneRouter::BackboneRouterConfig &aConfig);
+    void HandleBackboneRouterPrimaryUpdate(BackboneRouter::Leader::State aState, const BackboneRouter::Config &aConfig);
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
     /**
@@ -106,7 +106,7 @@
      * @param[in]  aOldMlrRegisteredAddressNum  The number of previously registered IPv6 addresses.
      *
      */
-    void UpdateProxiedSubscriptions(Child &             aChild,
+    void UpdateProxiedSubscriptions(Child              &aChild,
                                     const Ip6::Address *aOldMlrRegisteredAddresses,
                                     uint16_t            aOldMlrRegisteredAddressNum);
 #endif
@@ -134,42 +134,42 @@
      * @retval kErrorNoBufs        If insufficient message buffers available.
      *
      */
-    Error RegisterMulticastListeners(const otIp6Address *                    aAddresses,
+    Error RegisterMulticastListeners(const otIp6Address                     *aAddresses,
                                      uint8_t                                 aAddressNum,
-                                     const uint32_t *                        aTimeout,
+                                     const uint32_t                         *aTimeout,
                                      otIp6RegisterMulticastListenersCallback aCallback,
-                                     void *                                  aContext);
+                                     void                                   *aContext);
 #endif
 
 private:
     void HandleNotifierEvents(Events aEvents);
 
     void  SendMulticastListenerRegistration(void);
-    Error SendMulticastListenerRegistrationMessage(const otIp6Address *  aAddresses,
+    Error SendMulticastListenerRegistrationMessage(const otIp6Address   *aAddresses,
                                                    uint8_t               aAddressNum,
-                                                   const uint32_t *      aTimeout,
+                                                   const uint32_t       *aTimeout,
                                                    Coap::ResponseHandler aResponseHandler,
-                                                   void *                aResponseContext);
+                                                   void                 *aResponseContext);
 
-    static void  HandleMulticastListenerRegistrationResponse(void *               aContext,
-                                                             otMessage *          aMessage,
+    static void  HandleMulticastListenerRegistrationResponse(void                *aContext,
+                                                             otMessage           *aMessage,
                                                              const otMessageInfo *aMessageInfo,
                                                              Error                aResult);
-    void         HandleMulticastListenerRegistrationResponse(Coap::Message *         aMessage,
+    void         HandleMulticastListenerRegistrationResponse(Coap::Message          *aMessage,
                                                              const Ip6::MessageInfo *aMessageInfo,
                                                              Error                   aResult);
     static Error ParseMulticastListenerRegistrationResponse(Error          aResult,
                                                             Coap::Message *aMessage,
-                                                            uint8_t &      aStatus,
-                                                            Ip6::Address * aFailedAddresses,
-                                                            uint8_t &      aFailedAddressNum);
+                                                            uint8_t       &aStatus,
+                                                            Ip6::Address  *aFailedAddresses,
+                                                            uint8_t       &aFailedAddressNum);
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE
-    static void HandleRegisterMulticastListenersResponse(void *               aContext,
-                                                         otMessage *          aMessage,
+    static void HandleRegisterMulticastListenersResponse(void                *aContext,
+                                                         otMessage           *aMessage,
                                                          const otMessageInfo *aMessageInfo,
                                                          Error                aResult);
-    void        HandleRegisterMulticastListenersResponse(otMessage *          aMessage,
+    void        HandleRegisterMulticastListenersResponse(otMessage           *aMessage,
                                                          const otMessageInfo *aMessageInfo,
                                                          Error                aResult);
 #endif
@@ -193,7 +193,7 @@
                                              uint8_t             aFailedAddressNum);
 
     void        AppendToUniqueAddressList(Ip6::Address (&aAddresses)[Ip6AddressesTlv::kMaxAddresses],
-                                          uint8_t &           aAddressNum,
+                                          uint8_t            &aAddressNum,
                                           const Ip6::Address &aAddress);
     static bool AddressListContains(const Ip6::Address *aAddressList,
                                     uint8_t             aAddressListSize,
@@ -214,8 +214,7 @@
                                uint8_t             aFailedAddressNum);
 
 #if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE) && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE
-    otIp6RegisterMulticastListenersCallback mRegisterMulticastListenersCallback;
-    void *                                  mRegisterMulticastListenersContext;
+    Callback<otIp6RegisterMulticastListenersCallback> mRegisterMulticastListenersCallback;
 #endif
 
     uint32_t mReregistrationDelay;
diff --git a/src/core/thread/neighbor_table.cpp b/src/core/thread/neighbor_table.cpp
index f7856a1..9355473 100644
--- a/src/core/thread/neighbor_table.cpp
+++ b/src/core/thread/neighbor_table.cpp
@@ -136,7 +136,7 @@
 
 Neighbor *NeighborTable::FindNeighbor(const Ip6::Address &aIp6Address, Neighbor::StateFilter aFilter)
 {
-    Neighbor *   neighbor = nullptr;
+    Neighbor    *neighbor = nullptr;
     Mac::Address macAddress;
 
     if (aIp6Address.IsLinkLocal())
@@ -172,7 +172,7 @@
     Neighbor *neighbor = nullptr;
 
     VerifyOrExit(Get<Mle::Mle>().IsChild());
-    neighbor = Get<RouterTable>().GetNeighbor(aMacAddress);
+    neighbor = Get<RouterTable>().FindNeighbor(aMacAddress);
 
 exit:
     return neighbor;
@@ -213,7 +213,7 @@
 
     for (index = -aIterator; index <= Mle::kMaxRouterId; index++)
     {
-        Router *router = Get<RouterTable>().GetRouter(static_cast<uint8_t>(index));
+        Router *router = Get<RouterTable>().FindRouterById(static_cast<uint8_t>(index));
 
         if (router != nullptr && router->IsStateValid())
         {
@@ -271,6 +271,7 @@
         case kChildRemoved:
         case kChildModeChanged:
 #if OPENTHREAD_FTD
+            OT_ASSERT(Get<ChildTable>().Contains(aNeighbor));
             static_cast<Child::Info &>(info.mInfo.mChild).SetFrom(static_cast<const Child &>(aNeighbor));
 #endif
             break;
diff --git a/src/core/thread/neighbor_table.hpp b/src/core/thread/neighbor_table.hpp
index abe2df6..d1e31d5 100644
--- a/src/core/thread/neighbor_table.hpp
+++ b/src/core/thread/neighbor_table.hpp
@@ -124,7 +124,7 @@
      * @returns A pointer to the `Neighbor` corresponding to @p aMacAddress, `nullptr` otherwise.
      *
      */
-    Neighbor *FindParent(const Mac::Address &  aMacAddress,
+    Neighbor *FindParent(const Mac::Address   &aMacAddress,
                          Neighbor::StateFilter aFilter = Neighbor::kInStateValidOrRestoring);
 
     /**
@@ -160,7 +160,7 @@
      * @returns A pointer to the `Neighbor` corresponding to @p aMacAddress, `nullptr` otherwise.
      *
      */
-    Neighbor *FindNeighbor(const Mac::Address &  aMacAddress,
+    Neighbor *FindNeighbor(const Mac::Address   &aMacAddress,
                            Neighbor::StateFilter aFilter = Neighbor::kInStateValidOrRestoring);
 
 #if OPENTHREAD_FTD
@@ -174,7 +174,7 @@
      * @returns A pointer to the `Neighbor` corresponding to @p aIp6Address, `nullptr` otherwise.
      *
      */
-    Neighbor *FindNeighbor(const Ip6::Address &  aIp6Address,
+    Neighbor *FindNeighbor(const Ip6::Address   &aIp6Address,
                            Neighbor::StateFilter aFilter = Neighbor::kInStateValidOrRestoring);
 
     /**
diff --git a/src/core/thread/network_data.cpp b/src/core/thread/network_data.cpp
index 3c96a92..009e675 100644
--- a/src/core/thread/network_data.cpp
+++ b/src/core/thread/network_data.cpp
@@ -92,6 +92,7 @@
     config.mOnMeshPrefix  = &aConfig;
     config.mExternalRoute = nullptr;
     config.mService       = nullptr;
+    config.mLowpanContext = nullptr;
 
     return Iterate(aIterator, aRloc16, config);
 }
@@ -108,6 +109,7 @@
     config.mOnMeshPrefix  = nullptr;
     config.mExternalRoute = &aConfig;
     config.mService       = nullptr;
+    config.mLowpanContext = nullptr;
 
     return Iterate(aIterator, aRloc16, config);
 }
@@ -124,10 +126,23 @@
     config.mOnMeshPrefix  = nullptr;
     config.mExternalRoute = nullptr;
     config.mService       = &aConfig;
+    config.mLowpanContext = nullptr;
 
     return Iterate(aIterator, aRloc16, config);
 }
 
+Error NetworkData::GetNextLowpanContextInfo(Iterator &aIterator, LowpanContextInfo &aContextInfo) const
+{
+    Config config;
+
+    config.mOnMeshPrefix  = nullptr;
+    config.mExternalRoute = nullptr;
+    config.mService       = nullptr;
+    config.mLowpanContext = &aContextInfo;
+
+    return Iterate(aIterator, Mac::kShortAddrBroadcast, config);
+}
+
 Error NetworkData::Iterate(Iterator &aIterator, uint16_t aRloc16, Config &aConfig) const
 {
     // Iterate to the next entry in Network Data matching `aRloc16`
@@ -152,7 +167,8 @@
         switch (cur->GetType())
         {
         case NetworkDataTlv::kTypePrefix:
-            if ((aConfig.mOnMeshPrefix != nullptr) || (aConfig.mExternalRoute != nullptr))
+            if ((aConfig.mOnMeshPrefix != nullptr) || (aConfig.mExternalRoute != nullptr) ||
+                (aConfig.mLowpanContext != nullptr))
             {
                 subTlvs = As<PrefixTlv>(cur)->GetSubTlvs();
             }
@@ -174,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)
             {
@@ -199,6 +215,7 @@
 
                             aConfig.mExternalRoute = nullptr;
                             aConfig.mService       = nullptr;
+                            aConfig.mLowpanContext = nullptr;
                             aConfig.mOnMeshPrefix->SetFrom(*prefixTlv, *borderRouter, *borderRouterEntry);
 
                             ExitNow(error = kErrorNone);
@@ -223,8 +240,9 @@
                         {
                             const HasRouteEntry *hasRouteEntry = hasRoute->GetEntry(index);
 
-                            aConfig.mOnMeshPrefix = nullptr;
-                            aConfig.mService      = nullptr;
+                            aConfig.mOnMeshPrefix  = nullptr;
+                            aConfig.mService       = nullptr;
+                            aConfig.mLowpanContext = nullptr;
                             aConfig.mExternalRoute->SetFrom(GetInstance(), *prefixTlv, *hasRoute, *hasRouteEntry);
 
                             ExitNow(error = kErrorNone);
@@ -234,6 +252,29 @@
                     break;
                 }
 
+                case NetworkDataTlv::kTypeContext:
+                {
+                    const ContextTlv *contextTlv = As<ContextTlv>(subCur);
+
+                    if (aConfig.mLowpanContext == nullptr)
+                    {
+                        continue;
+                    }
+
+                    if (iterator.IsNewEntry())
+                    {
+                        aConfig.mOnMeshPrefix  = nullptr;
+                        aConfig.mExternalRoute = nullptr;
+                        aConfig.mService       = nullptr;
+                        aConfig.mLowpanContext->SetFrom(*prefixTlv, *contextTlv);
+
+                        iterator.MarkEntryAsNotNew();
+                        ExitNow(error = kErrorNone);
+                    }
+
+                    break;
+                }
+
                 default:
                     break;
                 }
@@ -260,6 +301,7 @@
                     {
                         aConfig.mOnMeshPrefix  = nullptr;
                         aConfig.mExternalRoute = nullptr;
+                        aConfig.mLowpanContext = nullptr;
                         aConfig.mService->SetFrom(*service, *server);
 
                         iterator.MarkEntryAsNotNew();
@@ -344,6 +386,7 @@
         config.mOnMeshPrefix  = &prefix;
         config.mExternalRoute = &route;
         config.mService       = &service;
+        config.mLowpanContext = nullptr;
 
         SuccessOrExit(aCompare.Iterate(iterator, aRloc16, config));
 
@@ -425,7 +468,7 @@
             case NetworkDataTlv::kTypeBorderRouter:
             {
                 BorderRouterTlv *borderRouter = As<BorderRouterTlv>(cur);
-                ContextTlv *     context      = aPrefix.FindSubTlv<ContextTlv>();
+                ContextTlv      *context      = aPrefix.FindSubTlv<ContextTlv>();
 
                 // Replace p_border_router_16
                 for (BorderRouterEntry *entry = borderRouter->GetFirstEntry(); entry <= borderRouter->GetLastEntry();
@@ -486,7 +529,7 @@
             switch (cur->GetType())
             {
             case NetworkDataTlv::kTypeServer:
-                As<ServerTlv>(cur)->SetServer16(Mle::Mle::ServiceAlocFromId(aService.GetServiceId()));
+                As<ServerTlv>(cur)->SetServer16(Mle::ServiceAlocFromId(aService.GetServiceId()));
                 break;
 
             default:
@@ -540,7 +583,7 @@
     return serviceTlv;
 }
 
-const ServiceTlv *NetworkData::FindNextService(const ServiceTlv * aPrevServiceTlv,
+const ServiceTlv *NetworkData::FindNextService(const ServiceTlv  *aPrevServiceTlv,
                                                uint32_t           aEnterpriseNumber,
                                                const ServiceData &aServiceData,
                                                ServiceMatchMode   aServiceMatchMode) const
@@ -562,14 +605,14 @@
     return NetworkData(GetInstance(), tlvs, length).FindService(aEnterpriseNumber, aServiceData, aServiceMatchMode);
 }
 
-const ServiceTlv *NetworkData::FindNextThreadService(const ServiceTlv * aPrevServiceTlv,
+const ServiceTlv *NetworkData::FindNextThreadService(const ServiceTlv  *aPrevServiceTlv,
                                                      const ServiceData &aServiceData,
                                                      ServiceMatchMode   aServiceMatchMode) const
 {
     return FindNextService(aPrevServiceTlv, ServiceTlv::kThreadEnterpriseNumber, aServiceData, aServiceMatchMode);
 }
 
-bool NetworkData::MatchService(const ServiceTlv & aServiceTlv,
+bool NetworkData::MatchService(const ServiceTlv  &aServiceTlv,
                                uint32_t           aEnterpriseNumber,
                                const ServiceData &aServiceData,
                                ServiceMatchMode   aServiceMatchMode)
@@ -630,46 +673,7 @@
     mLength -= aRemoveLength;
 }
 
-void MutableNetworkData::RemoveTlv(NetworkDataTlv *aTlv)
-{
-    Remove(aTlv, aTlv->GetSize());
-}
-
-Error NetworkData::SendServerDataNotification(uint16_t              aRloc16,
-                                              bool                  aAppendNetDataTlv,
-                                              Coap::ResponseHandler aHandler,
-                                              void *                aContext) const
-{
-    Error            error = kErrorNone;
-    Coap::Message *  message;
-    Tmf::MessageInfo messageInfo(GetInstance());
-
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kServerData);
-    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
-
-    if (aAppendNetDataTlv)
-    {
-        ThreadTlv tlv;
-        tlv.SetType(ThreadTlv::kThreadNetworkData);
-        tlv.SetLength(mLength);
-        SuccessOrExit(error = message->Append(tlv));
-        SuccessOrExit(error = message->AppendBytes(mTlvs, mLength));
-    }
-
-    if (aRloc16 != Mac::kShortAddrInvalid)
-    {
-        SuccessOrExit(error = Tlv::Append<ThreadRloc16Tlv>(*message, aRloc16));
-    }
-
-    IgnoreError(messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc());
-    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, aHandler, aContext));
-
-    LogInfo("Sent server data notification");
-
-exit:
-    FreeMessageOnError(message, error);
-    return error;
-}
+void MutableNetworkData::RemoveTlv(NetworkDataTlv *aTlv) { Remove(aTlv, aTlv->GetSize()); }
 
 Error NetworkData::GetNextServer(Iterator &aIterator, uint16_t &aRloc16) const
 {
@@ -682,6 +686,7 @@
     config.mOnMeshPrefix  = &prefixConfig;
     config.mExternalRoute = &routeConfig;
     config.mService       = &serviceConfig;
+    config.mLowpanContext = nullptr;
 
     SuccessOrExit(error = Iterate(aIterator, Mac::kShortAddrBroadcast, config));
 
@@ -736,11 +741,11 @@
                 break;
 
             case kRouterRoleOnly:
-                VerifyOrExit(Mle::Mle::IsActiveRouter(aRloc16));
+                VerifyOrExit(Mle::IsActiveRouter(aRloc16));
                 break;
 
             case kChildRoleOnly:
-                VerifyOrExit(!Mle::Mle::IsActiveRouter(aRloc16));
+                VerifyOrExit(!Mle::IsActiveRouter(aRloc16));
                 break;
             }
 
@@ -764,7 +769,7 @@
 
     private:
         RoleFilter mRoleFilter;
-        uint16_t * mRlocs;
+        uint16_t  *mRlocs;
         uint8_t    mLength;
         uint8_t    mMaxLength;
     };
diff --git a/src/core/thread/network_data.hpp b/src/core/thread/network_data.hpp
index cbe5133..3c1db30 100644
--- a/src/core/thread/network_data.hpp
+++ b/src/core/thread/network_data.hpp
@@ -267,6 +267,18 @@
     Error GetNextService(Iterator &aIterator, uint16_t aRloc16, ServiceConfig &aConfig) const;
 
     /**
+     * This method gets the next 6LoWPAN Context ID info in the Thread Network Data.
+     *
+     * @param[in,out]  aIterator     A reference to the Network Data iterator.
+     * @param[out]     aContextInfo  A reference to where the retrieved 6LoWPAN Context ID information will be placed.
+     *
+     * @retval kErrorNone      Successfully found the next 6LoWPAN Context ID info.
+     * @retval kErrorNotFound  No subsequent 6LoWPAN Context info exists in the partition's Network Data.
+     *
+     */
+    Error GetNextLowpanContextInfo(Iterator &aIterator, LowpanContextInfo &aContextInfo) const;
+
+    /**
      * This method indicates whether or not the Thread Network Data contains a given on mesh prefix entry.
      *
      * @param[in]  aPrefix   The on mesh prefix config to check.
@@ -465,7 +477,7 @@
      * @returns A pointer to the next matching Service TLV if one is found or `nullptr` if it cannot be found.
      *
      */
-    const ServiceTlv *FindNextService(const ServiceTlv * aPrevServiceTlv,
+    const ServiceTlv *FindNextService(const ServiceTlv  *aPrevServiceTlv,
                                       uint32_t           aEnterpriseNumber,
                                       const ServiceData &aServiceData,
                                       ServiceMatchMode   aServiceMatchMode) const;
@@ -484,27 +496,10 @@
      * @returns A pointer to the next matching Thread Service TLV if one is found or `nullptr` if it cannot be found.
      *
      */
-    const ServiceTlv *FindNextThreadService(const ServiceTlv * aPrevServiceTlv,
+    const ServiceTlv *FindNextThreadService(const ServiceTlv  *aPrevServiceTlv,
                                             const ServiceData &aServiceData,
                                             ServiceMatchMode   aServiceMatchMode) const;
 
-    /**
-     * This method sends a Server Data Notification message to the Leader.
-     *
-     * @param[in]  aRloc16            The old RLOC16 value that was previously registered.
-     * @param[in]  aAppendNetDataTlv  Indicates whether or not to append Thread Network Data TLV to the message.
-     * @param[in]  aHandler           A function pointer that is called when the transaction ends.
-     * @param[in]  aContext           A pointer to arbitrary context information.
-     *
-     * @retval kErrorNone     Successfully enqueued the notification message.
-     * @retval kErrorNoBufs   Insufficient message buffers to generate the notification message.
-     *
-     */
-    Error SendServerDataNotification(uint16_t              aRloc16,
-                                     bool                  aAppendNetDataTlv,
-                                     Coap::ResponseHandler aHandler,
-                                     void *                aContext) const;
-
 private:
     class NetworkDataIterator
     {
@@ -532,7 +527,7 @@
                                                             GetSubTlvOffset());
         }
 
-        void AdvaceSubTlv(const NetworkDataTlv *aSubTlvs)
+        void AdvanceSubTlv(const NetworkDataTlv *aSubTlvs)
         {
             SaveSubTlvOffset(GetSubTlv(aSubTlvs)->GetNext(), aSubTlvs);
             SetEntryIndex(0);
@@ -571,14 +566,15 @@
 
     struct Config
     {
-        OnMeshPrefixConfig * mOnMeshPrefix;
+        OnMeshPrefixConfig  *mOnMeshPrefix;
         ExternalRouteConfig *mExternalRoute;
-        ServiceConfig *      mService;
+        ServiceConfig       *mService;
+        LowpanContextInfo   *mLowpanContext;
     };
 
     Error Iterate(Iterator &aIterator, uint16_t aRloc16, Config &aConfig) const;
 
-    static bool MatchService(const ServiceTlv & aServiceTlv,
+    static bool MatchService(const ServiceTlv  &aServiceTlv,
                              uint32_t           aEnterpriseNumber,
                              const ServiceData &aServiceData,
                              ServiceMatchMode   aServiceMatchMode);
diff --git a/src/core/thread/network_data_leader.cpp b/src/core/thread/network_data_leader.cpp
index 0207c39..351ea69 100644
--- a/src/core/thread/network_data_leader.cpp
+++ b/src/core/thread/network_data_leader.cpp
@@ -60,13 +60,13 @@
     mVersion       = Random::NonCrypto::GetUint8();
     mStableVersion = Random::NonCrypto::GetUint8();
     SetLength(0);
-    Get<ot::Notifier>().Signal(kEventThreadNetdataChanged);
+    SignalNetDataChanged();
 }
 
 Error LeaderBase::GetServiceId(uint32_t           aEnterpriseNumber,
                                const ServiceData &aServiceData,
                                bool               aServerStable,
-                               uint8_t &          aServiceId) const
+                               uint8_t           &aServiceId) const
 {
     Error         error    = kErrorNotFound;
     Iterator      iterator = kIteratorInit;
@@ -102,7 +102,8 @@
             continue;
         }
 
-        if ((error == kErrorNotFound) || (config.mPreference > aConfig.mPreference))
+        if ((error == kErrorNotFound) || (config.mPreference > aConfig.mPreference) ||
+            (config.mPreference == aConfig.mPreference && config.GetPrefix() < aConfig.GetPrefix()))
         {
             aConfig = config;
             error   = kErrorNone;
@@ -112,8 +113,14 @@
     return error;
 }
 
-const PrefixTlv *LeaderBase::FindNextMatchingPrefix(const Ip6::Address &aAddress, const PrefixTlv *aPrevTlv) const
+const PrefixTlv *LeaderBase::FindNextMatchingPrefixTlv(const Ip6::Address &aAddress, const PrefixTlv *aPrevTlv) const
 {
+    // This method iterates over Prefix TLVs which match a given IPv6
+    // `aAddress`. If `aPrevTlv` is `nullptr` we start from the
+    // beginning. Otherwise, we search for a match after `aPrevTlv`.
+    // This method returns a pointer to the next matching Prefix TLV
+    // when found, or `nullptr` if no match is found.
+
     const PrefixTlv *prefixTlv;
     TlvIterator      tlvIterator((aPrevTlv == nullptr) ? GetTlvsStart() : aPrevTlv->GetNext(), GetTlvsEnd());
 
@@ -130,32 +137,31 @@
 
 Error LeaderBase::GetContext(const Ip6::Address &aAddress, Lowpan::Context &aContext) const
 {
-    const PrefixTlv * prefix = nullptr;
+    const PrefixTlv  *prefixTlv = nullptr;
     const ContextTlv *contextTlv;
 
     aContext.mPrefix.SetLength(0);
 
     if (Get<Mle::MleRouter>().IsMeshLocalAddress(aAddress))
     {
-        aContext.mPrefix.Set(Get<Mle::MleRouter>().GetMeshLocalPrefix());
-        aContext.mContextId    = Mle::kMeshLocalPrefixContextId;
-        aContext.mCompressFlag = true;
+        GetContextForMeshLocalPrefix(aContext);
     }
 
-    while ((prefix = FindNextMatchingPrefix(aAddress, prefix)) != nullptr)
+    while ((prefixTlv = FindNextMatchingPrefixTlv(aAddress, prefixTlv)) != nullptr)
     {
-        contextTlv = prefix->FindSubTlv<ContextTlv>();
+        contextTlv = prefixTlv->FindSubTlv<ContextTlv>();
 
         if (contextTlv == nullptr)
         {
             continue;
         }
 
-        if (prefix->GetPrefixLength() > aContext.mPrefix.GetLength())
+        if (prefixTlv->GetPrefixLength() > aContext.mPrefix.GetLength())
         {
-            aContext.mPrefix.Set(prefix->GetPrefix(), prefix->GetPrefixLength());
+            prefixTlv->CopyPrefixTo(aContext.mPrefix);
             aContext.mContextId    = contextTlv->GetContextId();
             aContext.mCompressFlag = contextTlv->IsCompress();
+            aContext.mIsValid      = true;
         }
     }
 
@@ -166,28 +172,27 @@
 {
     Error            error = kErrorNotFound;
     TlvIterator      tlvIterator(GetTlvsStart(), GetTlvsEnd());
-    const PrefixTlv *prefix;
+    const PrefixTlv *prefixTlv;
 
     if (aContextId == Mle::kMeshLocalPrefixContextId)
     {
-        aContext.mPrefix.Set(Get<Mle::MleRouter>().GetMeshLocalPrefix());
-        aContext.mContextId    = Mle::kMeshLocalPrefixContextId;
-        aContext.mCompressFlag = true;
+        GetContextForMeshLocalPrefix(aContext);
         ExitNow(error = kErrorNone);
     }
 
-    while ((prefix = tlvIterator.Iterate<PrefixTlv>()) != nullptr)
+    while ((prefixTlv = tlvIterator.Iterate<PrefixTlv>()) != nullptr)
     {
-        const ContextTlv *contextTlv = prefix->FindSubTlv<ContextTlv>();
+        const ContextTlv *contextTlv = prefixTlv->FindSubTlv<ContextTlv>();
 
         if ((contextTlv == nullptr) || (contextTlv->GetContextId() != aContextId))
         {
             continue;
         }
 
-        aContext.mPrefix.Set(prefix->GetPrefix(), prefix->GetPrefixLength());
+        prefixTlv->CopyPrefixTo(aContext.mPrefix);
         aContext.mContextId    = contextTlv->GetContextId();
         aContext.mCompressFlag = contextTlv->IsCompress();
+        aContext.mIsValid      = true;
         ExitNow(error = kErrorNone);
     }
 
@@ -195,62 +200,57 @@
     return error;
 }
 
+void LeaderBase::GetContextForMeshLocalPrefix(Lowpan::Context &aContext) const
+{
+    aContext.mPrefix.Set(Get<Mle::MleRouter>().GetMeshLocalPrefix());
+    aContext.mContextId    = Mle::kMeshLocalPrefixContextId;
+    aContext.mCompressFlag = true;
+    aContext.mIsValid      = true;
+}
+
 bool LeaderBase::IsOnMesh(const Ip6::Address &aAddress) const
 {
-    const PrefixTlv *prefix = nullptr;
-    bool             rval   = false;
+    const PrefixTlv *prefixTlv = nullptr;
+    bool             isOnMesh  = false;
 
-    VerifyOrExit(!Get<Mle::MleRouter>().IsMeshLocalAddress(aAddress), rval = true);
+    VerifyOrExit(!Get<Mle::MleRouter>().IsMeshLocalAddress(aAddress), isOnMesh = true);
 
-    while ((prefix = FindNextMatchingPrefix(aAddress, prefix)) != nullptr)
+    while ((prefixTlv = FindNextMatchingPrefixTlv(aAddress, prefixTlv)) != nullptr)
     {
-        // check both stable and temporary Border Router TLVs
-        for (int i = 0; i < 2; i++)
+        TlvIterator            subTlvIterator(*prefixTlv);
+        const BorderRouterTlv *brTlv;
+
+        while ((brTlv = subTlvIterator.Iterate<BorderRouterTlv>()) != nullptr)
         {
-            const BorderRouterTlv *borderRouter = prefix->FindSubTlv<BorderRouterTlv>(/* aStable */ (i == 0));
-
-            if (borderRouter == nullptr)
-            {
-                continue;
-            }
-
-            for (const BorderRouterEntry *entry = borderRouter->GetFirstEntry(); entry <= borderRouter->GetLastEntry();
+            for (const BorderRouterEntry *entry = brTlv->GetFirstEntry(); entry <= brTlv->GetLastEntry();
                  entry                          = entry->GetNext())
             {
                 if (entry->IsOnMesh())
                 {
-                    ExitNow(rval = true);
+                    ExitNow(isOnMesh = true);
                 }
             }
         }
     }
 
 exit:
-    return rval;
+    return isOnMesh;
 }
 
-Error LeaderBase::RouteLookup(const Ip6::Address &aSource,
-                              const Ip6::Address &aDestination,
-                              uint8_t *           aPrefixMatchLength,
-                              uint16_t *          aRloc16) const
+Error LeaderBase::RouteLookup(const Ip6::Address &aSource, const Ip6::Address &aDestination, uint16_t &aRloc16) const
 {
-    Error            error  = kErrorNoRoute;
-    const PrefixTlv *prefix = nullptr;
+    Error            error     = kErrorNoRoute;
+    const PrefixTlv *prefixTlv = nullptr;
 
-    while ((prefix = FindNextMatchingPrefix(aSource, prefix)) != nullptr)
+    while ((prefixTlv = FindNextMatchingPrefixTlv(aSource, prefixTlv)) != nullptr)
     {
-        if (ExternalRouteLookup(prefix->GetDomainId(), aDestination, aPrefixMatchLength, aRloc16) == kErrorNone)
+        if (ExternalRouteLookup(prefixTlv->GetDomainId(), aDestination, aRloc16) == kErrorNone)
         {
             ExitNow(error = kErrorNone);
         }
 
-        if (DefaultRouteLookup(*prefix, aRloc16) == kErrorNone)
+        if (DefaultRouteLookup(*prefixTlv, aRloc16) == kErrorNone)
         {
-            if (aPrefixMatchLength)
-            {
-                *aPrefixMatchLength = 0;
-            }
-
             ExitNow(error = kErrorNone);
         }
     }
@@ -259,18 +259,62 @@
     return error;
 }
 
-Error LeaderBase::ExternalRouteLookup(uint8_t             aDomainId,
-                                      const Ip6::Address &aDestination,
-                                      uint8_t *           aPrefixMatchLength,
-                                      uint16_t *          aRloc16) const
+template <typename EntryType>
+int LeaderBase::CompareRouteEntries(const EntryType &aFirst, const EntryType &aSecond) const
 {
-    Error                error = kErrorNoRoute;
-    TlvIterator          tlvIterator(GetTlvsStart(), GetTlvsEnd());
-    const PrefixTlv *    prefixTlv;
+    // `EntryType` can be `HasRouteEntry` or `BorderRouterEntry`.
+
+    return CompareRouteEntries(aFirst.GetPreference(), aFirst.GetRloc(), aSecond.GetPreference(), aSecond.GetRloc());
+}
+
+int LeaderBase::CompareRouteEntries(int8_t   aFirstPreference,
+                                    uint16_t aFirstRloc,
+                                    int8_t   aSecondPreference,
+                                    uint16_t aSecondRloc) const
+{
+    // Performs three-way comparison between two BR entries.
+
+    int result;
+
+    // Prefer the entry with higher preference.
+
+    result = ThreeWayCompare(aFirstPreference, aSecondPreference);
+    VerifyOrExit(result == 0);
+
+#if OPENTHREAD_MTD
+    // On MTD, prefer the BR that is this device itself. This handles
+    // the uncommon case where an MTD itself may be acting as BR.
+
+    result = ThreeWayCompare((aFirstRloc == Get<Mle::Mle>().GetRloc16()), (aSecondRloc == Get<Mle::Mle>().GetRloc16()));
+#endif
+
+#if OPENTHREAD_FTD
+    // If all the same, prefer the one with lower mesh path cost.
+    // Lower cost is preferred so we pass the second entry's cost as
+    // the first argument in the call to `ThreeWayCompare()`, i.e.,
+    // if the second entry's cost is larger, we return 1 indicating
+    // 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:
+    return result;
+}
+
+Error LeaderBase::ExternalRouteLookup(uint8_t aDomainId, const Ip6::Address &aDestination, uint16_t &aRloc16) const
+{
+    Error                error           = kErrorNoRoute;
+    const PrefixTlv     *prefixTlv       = nullptr;
     const HasRouteEntry *bestRouteEntry  = nullptr;
     uint8_t              bestMatchLength = 0;
 
-    while ((prefixTlv = tlvIterator.Iterate<PrefixTlv>()) != nullptr)
+    while ((prefixTlv = FindNextMatchingPrefixTlv(aDestination, prefixTlv)) != nullptr)
     {
         const HasRouteTlv *hasRoute;
         uint8_t            prefixLength = prefixTlv->GetPrefixLength();
@@ -281,11 +325,6 @@
             continue;
         }
 
-        if (!aDestination.MatchesPrefix(prefixTlv->GetPrefix(), prefixLength))
-        {
-            continue;
-        }
-
         if ((bestRouteEntry != nullptr) && (prefixLength <= bestMatchLength))
         {
             continue;
@@ -296,12 +335,8 @@
             for (const HasRouteEntry *entry = hasRoute->GetFirstEntry(); entry <= hasRoute->GetLastEntry();
                  entry                      = entry->GetNext())
             {
-                if (bestRouteEntry == nullptr || entry->GetPreference() > bestRouteEntry->GetPreference() ||
-                    (entry->GetPreference() == bestRouteEntry->GetPreference() &&
-                     (entry->GetRloc() == Get<Mle::MleRouter>().GetRloc16() ||
-                      (bestRouteEntry->GetRloc() != Get<Mle::MleRouter>().GetRloc16() &&
-                       Get<Mle::MleRouter>().GetCost(entry->GetRloc()) <
-                           Get<Mle::MleRouter>().GetCost(bestRouteEntry->GetRloc())))))
+                if ((bestRouteEntry == nullptr) || (prefixLength > bestMatchLength) ||
+                    CompareRouteEntries(*entry, *bestRouteEntry) > 0)
                 {
                     bestRouteEntry  = entry;
                     bestMatchLength = prefixLength;
@@ -312,32 +347,23 @@
 
     if (bestRouteEntry != nullptr)
     {
-        if (aRloc16 != nullptr)
-        {
-            *aRloc16 = bestRouteEntry->GetRloc();
-        }
-
-        if (aPrefixMatchLength != nullptr)
-        {
-            *aPrefixMatchLength = bestMatchLength;
-        }
-
-        error = kErrorNone;
+        aRloc16 = bestRouteEntry->GetRloc();
+        error   = kErrorNone;
     }
 
     return error;
 }
 
-Error LeaderBase::DefaultRouteLookup(const PrefixTlv &aPrefix, uint16_t *aRloc16) const
+Error LeaderBase::DefaultRouteLookup(const PrefixTlv &aPrefix, uint16_t &aRloc16) const
 {
     Error                    error = kErrorNoRoute;
     TlvIterator              subTlvIterator(aPrefix);
-    const BorderRouterTlv *  borderRouter;
+    const BorderRouterTlv   *brTlv;
     const BorderRouterEntry *route = nullptr;
 
-    while ((borderRouter = subTlvIterator.Iterate<BorderRouterTlv>()) != nullptr)
+    while ((brTlv = subTlvIterator.Iterate<BorderRouterTlv>()) != nullptr)
     {
-        for (const BorderRouterEntry *entry = borderRouter->GetFirstEntry(); entry <= borderRouter->GetLastEntry();
+        for (const BorderRouterEntry *entry = brTlv->GetFirstEntry(); entry <= brTlv->GetLastEntry();
              entry                          = entry->GetNext())
         {
             if (!entry->IsDefaultRoute())
@@ -345,11 +371,7 @@
                 continue;
             }
 
-            if (route == nullptr || entry->GetPreference() > route->GetPreference() ||
-                (entry->GetPreference() == route->GetPreference() &&
-                 (entry->GetRloc() == Get<Mle::MleRouter>().GetRloc16() ||
-                  (route->GetRloc() != Get<Mle::MleRouter>().GetRloc16() &&
-                   Get<Mle::MleRouter>().GetCost(entry->GetRloc()) < Get<Mle::MleRouter>().GetCost(route->GetRloc())))))
+            if (route == nullptr || CompareRouteEntries(*entry, *route) > 0)
             {
                 route = entry;
             }
@@ -358,12 +380,8 @@
 
     if (route != nullptr)
     {
-        if (aRloc16 != nullptr)
-        {
-            *aRloc16 = route->GetRloc();
-        }
-
-        error = kErrorNone;
+        aRloc16 = route->GetRloc();
+        error   = kErrorNone;
     }
 
     return error;
@@ -373,18 +391,15 @@
                                  uint8_t        aStableVersion,
                                  Type           aType,
                                  const Message &aMessage,
-                                 uint16_t       aMessageOffset)
+                                 uint16_t       aOffset,
+                                 uint16_t       aLength)
 {
-    Error    error = kErrorNone;
-    Mle::Tlv tlv;
-    uint16_t length;
+    Error error = kErrorNone;
 
-    SuccessOrExit(error = aMessage.Read(aMessageOffset, tlv));
+    VerifyOrExit(aLength <= kMaxSize, error = kErrorParse);
+    SuccessOrExit(error = aMessage.Read(aOffset, GetBytes(), aLength));
 
-    length = aMessage.ReadBytes(aMessageOffset + sizeof(tlv), GetBytes(), tlv.GetLength());
-    VerifyOrExit(length == tlv.GetLength(), error = kErrorParse);
-
-    SetLength(tlv.GetLength());
+    SetLength(static_cast<uint8_t>(aLength));
     mVersion       = aVersion;
     mStableVersion = aStableVersion;
 
@@ -402,7 +417,7 @@
 
     DumpDebg("SetNetworkData", GetBytes(), GetLength());
 
-    Get<ot::Notifier>().Signal(kEventThreadNetdataChanged);
+    SignalNetDataChanged();
 
 exit:
     return error;
@@ -427,7 +442,7 @@
     }
 
     mVersion++;
-    Get<ot::Notifier>().Signal(kEventThreadNetdataChanged);
+    SignalNetDataChanged();
 
 exit:
     return error;
@@ -440,7 +455,7 @@
 
 const MeshCoP::Tlv *LeaderBase::GetCommissioningDataSubTlv(MeshCoP::Tlv::Type aType) const
 {
-    const MeshCoP::Tlv *  rval = nullptr;
+    const MeshCoP::Tlv   *rval = nullptr;
     const NetworkDataTlv *commissioningDataTlv;
 
     commissioningDataTlv = GetCommissioningData();
@@ -488,7 +503,7 @@
 Error LeaderBase::SteeringDataCheck(const FilterIndexes &aFilterIndexes) const
 {
     Error                 error = kErrorNone;
-    const MeshCoP::Tlv *  steeringDataTlv;
+    const MeshCoP::Tlv   *steeringDataTlv;
     MeshCoP::SteeringData steeringData;
 
     steeringDataTlv = GetCommissioningDataSubTlv(MeshCoP::Tlv::kSteeringData);
@@ -522,5 +537,11 @@
     return SteeringDataCheck(filterIndexes);
 }
 
+void LeaderBase::SignalNetDataChanged(void)
+{
+    mMaxLength = Max(mMaxLength, GetLength());
+    Get<ot::Notifier>().Signal(kEventThreadNetdataChanged);
+}
+
 } // namespace NetworkData
 } // namespace ot
diff --git a/src/core/thread/network_data_leader.hpp b/src/core/thread/network_data_leader.hpp
index 54765ef..92dae93 100644
--- a/src/core/thread/network_data_leader.hpp
+++ b/src/core/thread/network_data_leader.hpp
@@ -74,6 +74,7 @@
      */
     explicit LeaderBase(Instance &aInstance)
         : MutableNetworkData(aInstance, mTlvBuffer, 0, sizeof(mTlvBuffer))
+        , mMaxLength(0)
     {
         Reset();
     }
@@ -85,6 +86,23 @@
     void Reset(void);
 
     /**
+     * This method returns the maximum observed Network Data length since OT stack initialization or since the last
+     * call to `ResetMaxLength()`.
+     *
+     * @returns The maximum observed Network Data length (high water mark for Network Data length).
+     *
+     */
+    uint8_t GetMaxLength(void) const { return mMaxLength; }
+
+    /**
+     * This method resets the tracked maximum Network Data Length.
+     *
+     * @sa GetMaxLength
+     *
+     */
+    void ResetMaxLength(void) { mMaxLength = GetLength(); }
+
+    /**
      * This method returns the Data Version value for a type (full set or stable subset).
      *
      * @param[in] aType   The Network Data type (full set or stable subset).
@@ -134,36 +152,34 @@
      *
      * @param[in]   aSource             A reference to the IPv6 source address.
      * @param[in]   aDestination        A reference to the IPv6 destination address.
-     * @param[out]  aPrefixMatchLength  A pointer to output the longest prefix match length in bits.
-     * @param[out]  aRloc16             A pointer to the RLOC16 for the selected route.
+     * @param[out]  aRloc16             A reference to return the RLOC16 for the selected route.
      *
-     * @retval kErrorNone      Successfully found a route.
+     * @retval kErrorNone      Successfully found a route. @p aRloc16 is updated.
      * @retval kErrorNoRoute   No valid route was found.
      *
      */
-    Error RouteLookup(const Ip6::Address &aSource,
-                      const Ip6::Address &aDestination,
-                      uint8_t *           aPrefixMatchLength,
-                      uint16_t *          aRloc16) const;
+    Error RouteLookup(const Ip6::Address &aSource, const Ip6::Address &aDestination, uint16_t &aRloc16) const;
 
     /**
-     * This method is used by non-Leader devices to set newly received Network Data from the Leader.
+     * This method is used by non-Leader devices to set Network Data by reading it from a message from Leader.
      *
      * @param[in]  aVersion        The Version value.
      * @param[in]  aStableVersion  The Stable Version value.
      * @param[in]  aType           The Network Data type to set, the full set or stable subset.
-     * @param[in]  aMessage        A reference to the MLE message.
-     * @param[in]  aMessageOffset  The offset in @p aMessage for the Network Data TLV.
+     * @param[in]  aMessage        A reference to the message.
+     * @param[in]  aOffset         The offset in @p aMessage pointing to start of Network Data.
+     * @param[in]  aLength         The length of Network Data.
      *
      * @retval kErrorNone   Successfully set the network data.
-     * @retval kErrorParse  Network Data TLV in @p aMessage is not valid.
+     * @retval kErrorParse  Network Data in @p aMessage is not valid.
      *
      */
     Error SetNetworkData(uint8_t        aVersion,
                          uint8_t        aStableVersion,
                          Type           aType,
                          const Message &aMessage,
-                         uint16_t       aMessageOffset);
+                         uint16_t       aOffset,
+                         uint16_t       aLength);
 
     /**
      * This method returns a pointer to the Commissioning Data.
@@ -265,7 +281,7 @@
     Error GetServiceId(uint32_t           aEnterpriseNumber,
                        const ServiceData &aServiceData,
                        bool               aServerStable,
-                       uint8_t &          aServiceId) const;
+                       uint8_t           &aServiceId) const;
 
     /**
      * This methods gets the preferred NAT64 prefix from network data.
@@ -282,24 +298,31 @@
     Error GetPreferredNat64Prefix(ExternalRouteConfig &aConfig) const;
 
 protected:
+    void SignalNetDataChanged(void);
+
     uint8_t mStableVersion;
     uint8_t mVersion;
 
 private:
     using FilterIndexes = MeshCoP::SteeringData::HashBitIndexes;
 
-    const PrefixTlv *FindNextMatchingPrefix(const Ip6::Address &aAddress, const PrefixTlv *aPrevTlv) const;
+    const PrefixTlv *FindNextMatchingPrefixTlv(const Ip6::Address &aAddress, const PrefixTlv *aPrevTlv) const;
 
     void RemoveCommissioningData(void);
 
-    Error ExternalRouteLookup(uint8_t             aDomainId,
-                              const Ip6::Address &aDestination,
-                              uint8_t *           aPrefixMatchLength,
-                              uint16_t *          aRloc16) const;
-    Error DefaultRouteLookup(const PrefixTlv &aPrefix, uint16_t *aRloc16) const;
+    template <typename EntryType> int CompareRouteEntries(const EntryType &aFirst, const EntryType &aSecond) const;
+    int                               CompareRouteEntries(int8_t   aFirstPreference,
+                                                          uint16_t aFirstRloc,
+                                                          int8_t   aSecondPreference,
+                                                          uint16_t aSecondRloc) const;
+
+    Error ExternalRouteLookup(uint8_t aDomainId, const Ip6::Address &aDestination, uint16_t &aRloc16) const;
+    Error DefaultRouteLookup(const PrefixTlv &aPrefix, uint16_t &aRloc16) const;
     Error SteeringDataCheck(const FilterIndexes &aFilterIndexes) const;
+    void  GetContextForMeshLocalPrefix(Lowpan::Context &aContext) const;
 
     uint8_t mTlvBuffer[kMaxSize];
+    uint8_t mMaxLength;
 };
 
 /**
diff --git a/src/core/thread/network_data_leader_ftd.cpp b/src/core/thread/network_data_leader_ftd.cpp
index dac504d..39a033d 100644
--- a/src/core/thread/network_data_leader_ftd.cpp
+++ b/src/core/thread/network_data_leader_ftd.cpp
@@ -61,10 +61,8 @@
 Leader::Leader(Instance &aInstance)
     : LeaderBase(aInstance)
     , mWaitingForNetDataSync(false)
-    , mTimer(aInstance, Leader::HandleTimer)
-    , mServerData(UriPath::kServerData, &Leader::HandleServerData, this)
-    , mCommissioningDataGet(UriPath::kCommissionerGet, &Leader::HandleCommissioningGet, this)
-    , mCommissioningDataSet(UriPath::kCommissionerSet, &Leader::HandleCommissioningSet, this)
+    , mContextIds(aInstance)
+    , mTimer(aInstance)
 {
     Reset();
 }
@@ -73,9 +71,7 @@
 {
     LeaderBase::Reset();
 
-    memset(reinterpret_cast<void *>(mContextLastUsed), 0, sizeof(mContextLastUsed));
-    mContextUsed         = 0;
-    mContextIdReuseDelay = kContextIdReuseDelay;
+    mContextIds.Clear();
 }
 
 void Leader::Start(Mle::LeaderStartMode aStartMode)
@@ -86,17 +82,6 @@
     {
         mTimer.Start(kMaxNetDataSyncWait);
     }
-
-    Get<Tmf::Agent>().AddResource(mServerData);
-    Get<Tmf::Agent>().AddResource(mCommissioningDataGet);
-    Get<Tmf::Agent>().AddResource(mCommissioningDataSet);
-}
-
-void Leader::Stop(void)
-{
-    Get<Tmf::Agent>().RemoveResource(mServerData);
-    Get<Tmf::Agent>().RemoveResource(mCommissioningDataGet);
-    Get<Tmf::Agent>().RemoveResource(mCommissioningDataSet);
 }
 
 void Leader::IncrementVersion(void)
@@ -131,7 +116,7 @@
     }
 
     mVersion++;
-    Get<ot::Notifier>().Signal(kEventThreadNetdataChanged);
+    SignalNetDataChanged();
 }
 
 void Leader::RemoveBorderRouter(uint16_t aRloc16, MatchMode aMatchMode)
@@ -142,19 +127,14 @@
     IncrementVersions(flags);
 }
 
-void Leader::HandleServerData(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<Leader *>(aContext)->HandleServerData(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void Leader::HandleServerData(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void Leader::HandleTmf<kUriServerData>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     ThreadNetworkDataTlv networkDataTlv;
     uint16_t             rloc16;
 
-    LogInfo("Received network data registration");
+    VerifyOrExit(Get<Mle::Mle>().IsLeader() && !mWaitingForNetDataSync);
 
-    VerifyOrExit(!mWaitingForNetDataSync);
+    LogInfo("Received %s", UriToString<kUriServerData>());
 
     VerifyOrExit(aMessageInfo.GetPeerAddr().GetIid().IsRoutingLocator());
 
@@ -182,18 +162,13 @@
 
     SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
 
-    LogInfo("Sent network data registration acknowledgment");
+    LogInfo("Sent %s ack", UriToString<kUriServerData>());
 
 exit:
     return;
 }
 
-void Leader::HandleCommissioningSet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<Leader *>(aContext)->HandleCommissioningSet(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void Leader::HandleCommissioningSet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void Leader::HandleTmf<kUriCommissionerSet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     uint16_t                 offset = aMessage.GetOffset();
     uint16_t                 length = aMessage.GetLength() - aMessage.GetOffset();
@@ -202,13 +177,14 @@
     bool                     hasSessionId = false;
     bool                     hasValidTlv  = false;
     uint16_t                 sessionId    = 0;
-    CommissioningDataTlv *   commDataTlv;
+    CommissioningDataTlv    *commDataTlv;
 
     MeshCoP::Tlv *cur;
     MeshCoP::Tlv *end;
 
+    VerifyOrExit(Get<Mle::Mle>().IsLeader() && !mWaitingForNetDataSync);
+
     VerifyOrExit(length <= sizeof(tlvs));
-    VerifyOrExit(Get<Mle::MleRouter>().IsLeader());
 
     aMessage.ReadBytes(offset, tlvs, length);
 
@@ -290,31 +266,31 @@
     }
 }
 
-void Leader::HandleCommissioningGet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<Leader *>(aContext)->HandleCommissioningGet(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void Leader::HandleCommissioningGet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void Leader::HandleTmf<kUriCommissionerGet>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     uint16_t length = 0;
     uint16_t offset;
 
+    VerifyOrExit(Get<Mle::Mle>().IsLeader() && !mWaitingForNetDataSync);
+
     SuccessOrExit(Tlv::FindTlvValueOffset(aMessage, MeshCoP::Tlv::kGet, offset, length));
     aMessage.SetOffset(offset);
 
 exit:
-    SendCommissioningGetResponse(aMessage, length, aMessageInfo);
+    if (Get<Mle::MleRouter>().IsLeader())
+    {
+        SendCommissioningGetResponse(aMessage, length, aMessageInfo);
+    }
 }
 
-void Leader::SendCommissioningGetResponse(const Coap::Message &   aRequest,
+void Leader::SendCommissioningGetResponse(const Coap::Message    &aRequest,
                                           uint16_t                aLength,
                                           const Ip6::MessageInfo &aMessageInfo)
 {
     Error                 error = kErrorNone;
-    Coap::Message *       message;
+    Coap::Message        *message;
     CommissioningDataTlv *commDataTlv;
-    uint8_t *             data   = nullptr;
+    uint8_t              *data   = nullptr;
     uint8_t               length = 0;
 
     message = Get<Tmf::Agent>().NewPriorityResponseMessage(aRequest);
@@ -356,14 +332,14 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, aMessageInfo));
 
-    LogInfo("sent commissioning dataset get response");
+    LogInfo("Sent %s response", UriToString<kUriCommissionerGet>());
 
 exit:
     FreeMessageOnError(message, error);
 }
 
-void Leader::SendCommissioningSetResponse(const Coap::Message &    aRequest,
-                                          const Ip6::MessageInfo & aMessageInfo,
+void Leader::SendCommissioningSetResponse(const Coap::Message     &aRequest,
+                                          const Ip6::MessageInfo  &aMessageInfo,
                                           MeshCoP::StateTlv::State aState)
 {
     Error          error = kErrorNone;
@@ -376,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);
@@ -393,7 +369,7 @@
         break;
 
     case kMatchModeRouterId:
-        matched = Mle::Mle::RouterIdMatch(aFirstRloc16, aSecondRloc16);
+        matched = Mle::RouterIdMatch(aFirstRloc16, aSecondRloc16);
         break;
     }
 
@@ -664,15 +640,9 @@
     return contains;
 }
 
-Leader::UpdateStatus Leader::UpdatePrefix(PrefixTlv &aPrefix)
-{
-    return UpdateTlv(aPrefix, aPrefix.GetSubTlvs());
-}
+Leader::UpdateStatus Leader::UpdatePrefix(PrefixTlv &aPrefix) { return UpdateTlv(aPrefix, aPrefix.GetSubTlvs()); }
 
-Leader::UpdateStatus Leader::UpdateService(ServiceTlv &aService)
-{
-    return UpdateTlv(aService, aService.GetSubTlvs());
-}
+Leader::UpdateStatus Leader::UpdateService(ServiceTlv &aService) { return UpdateTlv(aService, aService.GetSubTlvs()); }
 
 Leader::UpdateStatus Leader::UpdateTlv(NetworkDataTlv &aTlv, const NetworkDataTlv *aSubTlvs)
 {
@@ -707,7 +677,7 @@
     Error        error = kErrorNone;
     ChangedFlags flags;
 
-    VerifyOrExit(Get<RouterTable>().IsAllocated(Mle::Mle::RouterIdFromRloc16(aRloc16)), error = kErrorNoRoute);
+    VerifyOrExit(Get<RouterTable>().IsAllocated(Mle::RouterIdFromRloc16(aRloc16)), error = kErrorNoRoute);
 
     // Validate that the `aNetworkData` contains well-formed TLVs, sub-TLVs,
     // and entries all matching `aRloc16` (no other RLOCs).
@@ -735,11 +705,10 @@
         }
     }
 
-    IncrementVersions(flags);
-
     DumpDebg("Register", GetBytes(), GetLength());
 
 exit:
+    IncrementVersions(flags);
 
     if (error != kErrorNone)
     {
@@ -795,7 +764,7 @@
 Error Leader::AddService(const ServiceTlv &aService, ChangedFlags &aChangedFlags)
 {
     Error            error = kErrorNone;
-    ServiceTlv *     dstService;
+    ServiceTlv      *dstService;
     ServiceData      serviceData;
     const ServerTlv *server;
 
@@ -838,7 +807,7 @@
 Error Leader::AddHasRoute(const HasRouteTlv &aHasRoute, PrefixTlv &aDstPrefix, ChangedFlags &aChangedFlags)
 {
     Error                error       = kErrorNone;
-    HasRouteTlv *        dstHasRoute = aDstPrefix.FindSubTlv<HasRouteTlv>(aHasRoute.IsStable());
+    HasRouteTlv         *dstHasRoute = aDstPrefix.FindSubTlv<HasRouteTlv>(aHasRoute.IsStable());
     const HasRouteEntry *entry       = aHasRoute.GetFirstEntry();
 
     if (dstHasRoute == nullptr)
@@ -875,17 +844,17 @@
 Error Leader::AddBorderRouter(const BorderRouterTlv &aBorderRouter, PrefixTlv &aDstPrefix, ChangedFlags &aChangedFlags)
 {
     Error                    error           = kErrorNone;
-    BorderRouterTlv *        dstBorderRouter = aDstPrefix.FindSubTlv<BorderRouterTlv>(aBorderRouter.IsStable());
-    ContextTlv *             dstContext      = aDstPrefix.FindSubTlv<ContextTlv>();
+    BorderRouterTlv         *dstBorderRouter = aDstPrefix.FindSubTlv<BorderRouterTlv>(aBorderRouter.IsStable());
+    ContextTlv              *dstContext      = aDstPrefix.FindSubTlv<ContextTlv>();
     uint8_t                  contextId       = 0;
     const BorderRouterEntry *entry           = aBorderRouter.GetFirstEntry();
 
     if (dstContext == nullptr)
     {
-        // Allocate a Context ID first. This ensure that if we cannot
-        // allocate, we fail and exit before potentially inserting a
-        // Border Router sub-TLV.
-        SuccessOrExit(error = AllocateContextId(contextId));
+        // Get a new Context ID first. This ensure that if we cannot
+        // get new Context ID, we fail and exit before potentially
+        // inserting a Border Router sub-TLV.
+        SuccessOrExit(error = mContextIds.GetUnallocatedId(contextId));
     }
 
     if (dstBorderRouter == nullptr)
@@ -924,7 +893,7 @@
     }
 
     dstContext->SetCompress();
-    StopContextReuseTimer(dstContext->GetContextId());
+    mContextIds.MarkAsInUse(dstContext->GetContextId());
 
     VerifyOrExit(!ContainsMatchingEntry(dstBorderRouter, *entry));
 
@@ -1004,50 +973,6 @@
     return service;
 }
 
-Error Leader::AllocateContextId(uint8_t &aContextId)
-{
-    Error error = kErrorNotFound;
-
-    for (uint8_t contextId = kMinContextId; contextId < kMinContextId + kNumContextIds; contextId++)
-    {
-        if ((mContextUsed & (1 << contextId)) == 0)
-        {
-            mContextUsed |= (1 << contextId);
-            aContextId = contextId;
-            error      = kErrorNone;
-            LogInfo("Allocated Context ID = %d", contextId);
-            break;
-        }
-    }
-
-    return error;
-}
-
-void Leader::FreeContextId(uint8_t aContextId)
-{
-    LogInfo("Free Context Id = %d", aContextId);
-    RemoveContext(aContextId);
-    mContextUsed &= ~(1 << aContextId);
-    IncrementVersions(/* aIncludeStable */ true);
-}
-
-void Leader::StartContextReuseTimer(uint8_t aContextId)
-{
-    mContextLastUsed[aContextId - kMinContextId] = TimerMilli::GetNow();
-
-    if (mContextLastUsed[aContextId - kMinContextId].GetValue() == 0)
-    {
-        mContextLastUsed[aContextId - kMinContextId].SetValue(1);
-    }
-
-    mTimer.Start(kStateUpdatePeriod);
-}
-
-void Leader::StopContextReuseTimer(uint8_t aContextId)
-{
-    mContextLastUsed[aContextId - kMinContextId].SetValue(0);
-}
-
 void Leader::RemoveRloc(uint16_t aRloc16, MatchMode aMatchMode, ChangedFlags &aChangedFlags)
 {
     NetworkData excludeNetworkData(GetInstance()); // Empty network data.
@@ -1058,7 +983,7 @@
 void Leader::RemoveRloc(uint16_t           aRloc16,
                         MatchMode          aMatchMode,
                         const NetworkData &aExcludeNetworkData,
-                        ChangedFlags &     aChangedFlags)
+                        ChangedFlags      &aChangedFlags)
 {
     // Remove entries from Network Data matching `aRloc16` (using
     // `aMatchMode` to determine the match) but exclude any entries
@@ -1074,7 +999,7 @@
         {
         case NetworkDataTlv::kTypePrefix:
         {
-            PrefixTlv *      prefix = As<PrefixTlv>(cur);
+            PrefixTlv       *prefix = As<PrefixTlv>(cur);
             const PrefixTlv *excludePrefix =
                 aExcludeNetworkData.FindPrefix(prefix->GetPrefix(), prefix->GetPrefixLength());
 
@@ -1091,7 +1016,7 @@
 
         case NetworkDataTlv::kTypeService:
         {
-            ServiceTlv *      service = As<ServiceTlv>(cur);
+            ServiceTlv       *service = As<ServiceTlv>(cur);
             ServiceData       serviceData;
             const ServiceTlv *excludeService;
 
@@ -1119,17 +1044,17 @@
     }
 }
 
-void Leader::RemoveRlocInPrefix(PrefixTlv &      aPrefix,
+void Leader::RemoveRlocInPrefix(PrefixTlv       &aPrefix,
                                 uint16_t         aRloc16,
                                 MatchMode        aMatchMode,
                                 const PrefixTlv *aExcludePrefix,
-                                ChangedFlags &   aChangedFlags)
+                                ChangedFlags    &aChangedFlags)
 {
     // Remove entries in `aPrefix` TLV matching the given `aRloc16`
     // excluding any entries that are present in `aExcludePrefix`.
 
     NetworkDataTlv *cur = aPrefix.GetSubTlvs();
-    ContextTlv *    context;
+    ContextTlv     *context;
 
     while (cur < aPrefix.GetNext())
     {
@@ -1169,30 +1094,30 @@
 
     if ((context = aPrefix.FindSubTlv<ContextTlv>()) != nullptr)
     {
-        if (aPrefix.GetSubTlvsLength() == sizeof(ContextTlv))
+        if (aPrefix.FindSubTlv<BorderRouterTlv>() == nullptr)
         {
             context->ClearCompress();
-            StartContextReuseTimer(context->GetContextId());
+            mContextIds.ScheduleToRemove(context->GetContextId());
         }
         else
         {
             context->SetCompress();
-            StopContextReuseTimer(context->GetContextId());
+            mContextIds.MarkAsInUse(context->GetContextId());
         }
     }
 }
 
-void Leader::RemoveRlocInService(ServiceTlv &      aService,
+void Leader::RemoveRlocInService(ServiceTlv       &aService,
                                  uint16_t          aRloc16,
                                  MatchMode         aMatchMode,
                                  const ServiceTlv *aExcludeService,
-                                 ChangedFlags &    aChangedFlags)
+                                 ChangedFlags     &aChangedFlags)
 {
     // Remove entries in `aService` TLV matching the given `aRloc16`
     // excluding any entries that are present in `aExcludeService`.
 
     NetworkDataTlv *start = aService.GetSubTlvs();
-    ServerTlv *     server;
+    ServerTlv      *server;
 
     while ((server = NetworkDataTlv::Find<ServerTlv>(start, aService.GetNext())) != nullptr)
     {
@@ -1210,12 +1135,12 @@
     }
 }
 
-void Leader::RemoveRlocInHasRoute(PrefixTlv &      aPrefix,
-                                  HasRouteTlv &    aHasRoute,
+void Leader::RemoveRlocInHasRoute(PrefixTlv       &aPrefix,
+                                  HasRouteTlv     &aHasRoute,
                                   uint16_t         aRloc16,
                                   MatchMode        aMatchMode,
                                   const PrefixTlv *aExcludePrefix,
-                                  ChangedFlags &   aChangedFlags)
+                                  ChangedFlags    &aChangedFlags)
 {
     // Remove entries in `aHasRoute` (a sub-TLV of `aPrefix` TLV)
     // matching the given `aRloc16` excluding entries that are present
@@ -1239,12 +1164,12 @@
     }
 }
 
-void Leader::RemoveRlocInBorderRouter(PrefixTlv &      aPrefix,
+void Leader::RemoveRlocInBorderRouter(PrefixTlv       &aPrefix,
                                       BorderRouterTlv &aBorderRouter,
                                       uint16_t         aRloc16,
                                       MatchMode        aMatchMode,
                                       const PrefixTlv *aExcludePrefix,
-                                      ChangedFlags &   aChangedFlags)
+                                      ChangedFlags    &aChangedFlags)
 {
     // Remove entries in `aBorderRouter` (a sub-TLV of `aPrefix` TLV)
     // matching the given `aRloc16` excluding entries that are present
@@ -1271,7 +1196,7 @@
 void Leader::RemoveContext(uint8_t aContextId)
 {
     NetworkDataTlv *start = GetTlvsStart();
-    PrefixTlv *     prefix;
+    PrefixTlv      *prefix;
 
     while ((prefix = NetworkDataTlv::Find<PrefixTlv>(start, GetTlvsEnd())) != nullptr)
     {
@@ -1285,12 +1210,14 @@
 
         start = prefix->GetNext();
     }
+
+    IncrementVersions(/* aIncludeStable */ true);
 }
 
 void Leader::RemoveContext(PrefixTlv &aPrefix, uint8_t aContextId)
 {
     NetworkDataTlv *start = aPrefix.GetSubTlvs();
-    ContextTlv *    context;
+    ContextTlv     *context;
 
     while ((context = NetworkDataTlv::Find<ContextTlv>(start, aPrefix.GetNext())) != nullptr)
     {
@@ -1325,82 +1252,26 @@
             continue;
         }
 
-        mContextUsed |= 1 << context->GetContextId();
+        mContextIds.MarkAsInUse(context->GetContextId());
 
-        if (context->IsCompress())
+        if (!context->IsCompress())
         {
-            StopContextReuseTimer(context->GetContextId());
-        }
-        else
-        {
-            StartContextReuseTimer(context->GetContextId());
+            mContextIds.ScheduleToRemove(context->GetContextId());
         }
     }
 }
 
-void Leader::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Leader>().HandleTimer();
-}
-
 void Leader::HandleTimer(void)
 {
-    bool contextsWaiting = false;
-
     if (mWaitingForNetDataSync)
     {
         LogInfo("Timed out waiting for netdata on restoring leader role after reset");
         IgnoreError(Get<Mle::MleRouter>().BecomeDetached());
-        ExitNow();
     }
-
-    for (uint8_t i = 0; i < kNumContextIds; i++)
+    else
     {
-        if (mContextLastUsed[i].GetValue() == 0)
-        {
-            continue;
-        }
-
-        if (TimerMilli::GetNow() - mContextLastUsed[i] >= Time::SecToMsec(mContextIdReuseDelay))
-        {
-            FreeContextId(kMinContextId + i);
-        }
-        else
-        {
-            contextsWaiting = true;
-        }
+        mContextIds.HandleTimer();
     }
-
-    if (contextsWaiting)
-    {
-        mTimer.Start(kStateUpdatePeriod);
-    }
-
-exit:
-    return;
-}
-
-Error Leader::RemoveStaleChildEntries(Coap::ResponseHandler aHandler, void *aContext)
-{
-    Error    error    = kErrorNotFound;
-    Iterator iterator = kIteratorInit;
-    uint16_t rloc16;
-
-    VerifyOrExit(Get<Mle::MleRouter>().IsRouterOrLeader());
-
-    while (GetNextServer(iterator, rloc16) == kErrorNone)
-    {
-        if (!Mle::Mle::IsActiveRouter(rloc16) && Mle::Mle::RouterIdMatch(Get<Mle::MleRouter>().GetRloc16(), rloc16) &&
-            Get<ChildTable>().FindChild(rloc16, Child::kInStateValid) == nullptr)
-        {
-            // In Thread 1.1 Specification 5.15.6.1, only one RLOC16 TLV entry may appear in SRV_DATA.ntf.
-            error = SendServerDataNotification(rloc16, /* aAppendNetDataTlv */ false, aHandler, aContext);
-            ExitNow();
-        }
-    }
-
-exit:
-    return error;
 }
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
@@ -1442,6 +1313,86 @@
 }
 #endif
 
+//---------------------------------------------------------------------------------------------------------------------
+// Leader::ContextIds
+
+void Leader::ContextIds::Clear(void)
+{
+    for (uint8_t id = kMinId; id <= kMaxId; id++)
+    {
+        MarkAsUnallocated(id);
+    }
+}
+
+Error Leader::ContextIds::GetUnallocatedId(uint8_t &aId)
+{
+    Error error = kErrorNotFound;
+
+    for (uint8_t id = kMinId; id <= kMaxId; id++)
+    {
+        if (IsUnallocated(id))
+        {
+            aId   = id;
+            error = kErrorNone;
+            break;
+        }
+    }
+
+    return error;
+}
+
+void Leader::ContextIds::ScheduleToRemove(uint8_t aId)
+{
+    VerifyOrExit(IsInUse(aId));
+
+    SetRemoveTime(aId, TimerMilli::GetNow() + Time::SecToMsec(mReuseDelay));
+    Get<Leader>().mTimer.FireAtIfEarlier(GetRemoveTime(aId));
+
+exit:
+    return;
+}
+
+void Leader::ContextIds::SetRemoveTime(uint8_t aId, TimeMilli aTime)
+{
+    uint32_t time = aTime.GetValue();
+
+    while ((time == kUnallocated) || (time == kInUse))
+    {
+        time++;
+    }
+
+    mRemoveTimes[aId - kMinId].SetValue(time);
+}
+
+void Leader::ContextIds::HandleTimer(void)
+{
+    TimeMilli now      = TimerMilli::GetNow();
+    TimeMilli nextTime = now.GetDistantFuture();
+
+    for (uint8_t id = kMinId; id <= kMaxId; id++)
+    {
+        if (IsUnallocated(id) || IsInUse(id))
+        {
+            continue;
+        }
+
+        if (now >= GetRemoveTime(id))
+        {
+            MarkAsUnallocated(id);
+            Get<Leader>().RemoveContext(id);
+        }
+        else
+        {
+            nextTime = Min(nextTime, GetRemoveTime(id));
+        }
+    }
+
+    if (nextTime != now.GetDistantFuture())
+    {
+        Get<Leader>().mTimer.FireAt(nextTime);
+    }
+}
+
 } // namespace NetworkData
 } // namespace ot
 
diff --git a/src/core/thread/network_data_leader_ftd.hpp b/src/core/thread/network_data_leader_ftd.hpp
index 1ca969f..85096c3 100644
--- a/src/core/thread/network_data_leader_ftd.hpp
+++ b/src/core/thread/network_data_leader_ftd.hpp
@@ -40,12 +40,13 @@
 
 #include <stdint.h>
 
-#include "coap/coap.hpp"
 #include "common/non_copyable.hpp"
+#include "common/numeric_limits.hpp"
 #include "common/timer.hpp"
 #include "net/ip6_address.hpp"
 #include "thread/mle_router.hpp"
 #include "thread/network_data.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -67,6 +68,8 @@
  */
 class Leader : public LeaderBase, private NonCopyable
 {
+    friend class Tmf::Agent;
+
 public:
     /**
      * This enumeration defines the match mode constants to compare two RLOC16 values.
@@ -106,12 +109,6 @@
     void Start(Mle::LeaderStartMode aStartMode);
 
     /**
-     * This method stops the Leader services.
-     *
-     */
-    void Stop(void);
-
-    /**
      * This method increments the Thread Network Data version.
      *
      */
@@ -126,20 +123,20 @@
     /**
      * This method returns CONTEXT_ID_RESUSE_DELAY value.
      *
-     * @returns The CONTEXT_ID_REUSE_DELAY value.
+     * @returns The CONTEXT_ID_REUSE_DELAY value (in seconds).
      *
      */
-    uint32_t GetContextIdReuseDelay(void) const { return mContextIdReuseDelay; }
+    uint32_t GetContextIdReuseDelay(void) const { return mContextIds.GetReuseDelay(); }
 
     /**
      * This method sets CONTEXT_ID_RESUSE_DELAY value.
      *
      * @warning This method should only be used for testing.
      *
-     * @param[in]  aDelay  The CONTEXT_ID_REUSE_DELAY value.
+     * @param[in]  aDelay  The CONTEXT_ID_REUSE_DELAY value (in seconds).
      *
      */
-    void SetContextIdReuseDelay(uint32_t aDelay) { mContextIdReuseDelay = aDelay; }
+    void SetContextIdReuseDelay(uint32_t aDelay) { mContextIds.SetReuseDelay(aDelay); }
 
     /**
      * This method removes Network Data entries matching with a given RLOC16.
@@ -167,19 +164,6 @@
      */
     const ServiceTlv *FindServiceById(uint8_t aServiceId) const;
 
-    /**
-     * This method sends SVR_DATA.ntf message for any stale child entries that exist in the network data.
-     *
-     * @param[in]  aHandler  A function pointer that is called when the transaction ends.
-     * @param[in]  aContext  A pointer to arbitrary context information.
-     *
-     * @retval kErrorNone      A stale child entry was found and successfully enqueued a SVR_DATA.ntf message.
-     * @retval kErrorNoBufs    A stale child entry was found, but insufficient message buffers were available.
-     * @retval kErrorNotFound  No stale child entries were found.
-     *
-     */
-    Error RemoveStaleChildEntries(Coap::ResponseHandler aHandler, void *aContext);
-
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     /**
      * This method indicates whether a given Prefix can act as a valid OMR prefix and exists in the network data.
@@ -194,6 +178,8 @@
 #endif
 
 private:
+    static constexpr uint32_t kMaxNetDataSyncWait = 60 * 1000; // Maximum time to wait for netdata sync in msec.
+
     class ChangedFlags
     {
     public:
@@ -223,11 +209,58 @@
         kTlvUpdated, // TLV stable flag is updated based on its sub TLVs.
     };
 
-    static void HandleServerData(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleServerData(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    class ContextIds : public InstanceLocator
+    {
+    public:
+        // This class tracks Context IDs. A Context ID can be in one
+        // of the 3 states: It is unallocated, or it is allocated
+        // and in-use, or it scheduled to be removed (after reuse delay
+        // interval is passed).
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+        static constexpr uint8_t kInvalidId = NumericLimits<uint8_t>::kMax;
+
+        explicit ContextIds(Instance &aInstance)
+            : InstanceLocator(aInstance)
+            , mReuseDelay(kReuseDelay)
+        {
+        }
+
+        void     Clear(void);
+        Error    GetUnallocatedId(uint8_t &aId);
+        void     MarkAsInUse(uint8_t aId) { mRemoveTimes[aId - kMinId].SetValue(kInUse); }
+        void     ScheduleToRemove(uint8_t aId);
+        uint32_t GetReuseDelay(void) const { return mReuseDelay; }
+        void     SetReuseDelay(uint32_t aDelay) { mReuseDelay = aDelay; }
+        void     HandleTimer(void);
+
+    private:
+        static constexpr uint32_t kReuseDelay = 5 * 60; // 5 minutes (in seconds).
+
+        static constexpr uint8_t kMinId = 1;
+        static constexpr uint8_t kMaxId = 15;
+
+        // The `mRemoveTimes[id]` is used to track the state of a
+        // Context ID and its remove time. Two specific values
+        // `kUnallocated` and `kInUse` are used to indicate ID is in
+        // unallocated or in-use states. Other values indicate we
+        // are in remove state waiting to remove it at `mRemoveTime`.
+
+        static constexpr uint32_t kUnallocated = 0;
+        static constexpr uint32_t kInUse       = 1;
+
+        bool      IsUnallocated(uint8_t aId) const { return mRemoveTimes[aId - kMinId].GetValue() == kUnallocated; }
+        bool      IsInUse(uint8_t aId) const { return mRemoveTimes[aId - kMinId].GetValue() == kInUse; }
+        TimeMilli GetRemoveTime(uint8_t aId) const { return mRemoveTimes[aId - kMinId]; }
+        void      SetRemoveTime(uint8_t aId, TimeMilli aTime);
+        void      MarkAsUnallocated(uint8_t aId) { mRemoveTimes[aId - kMinId].SetValue(kUnallocated); }
+
+        TimeMilli mRemoveTimes[kMaxId - kMinId + 1];
+        uint32_t  mReuseDelay;
+    };
+
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+
+    void HandleTimer(void);
 
     void RegisterNetworkData(uint16_t aRloc16, const NetworkData &aNetworkData);
 
@@ -239,11 +272,6 @@
 
     Error AllocateServiceId(uint8_t &aServiceId) const;
 
-    Error AllocateContextId(uint8_t &aContextId);
-    void  FreeContextId(uint8_t aContextId);
-    void  StartContextReuseTimer(uint8_t aContextId);
-    void  StopContextReuseTimer(uint8_t aContextId);
-
     void RemoveContext(uint8_t aContextId);
     void RemoveContext(PrefixTlv &aPrefix, uint8_t aContextId);
 
@@ -253,29 +281,29 @@
     void RemoveRloc(uint16_t           aRloc16,
                     MatchMode          aMatchMode,
                     const NetworkData &aExcludeNetworkData,
-                    ChangedFlags &     aChangedFlags);
-    void RemoveRlocInPrefix(PrefixTlv &      aPrefix,
+                    ChangedFlags      &aChangedFlags);
+    void RemoveRlocInPrefix(PrefixTlv       &aPrefix,
                             uint16_t         aRloc16,
                             MatchMode        aMatchMode,
                             const PrefixTlv *aExcludePrefix,
-                            ChangedFlags &   aChangedFlags);
-    void RemoveRlocInService(ServiceTlv &      aService,
+                            ChangedFlags    &aChangedFlags);
+    void RemoveRlocInService(ServiceTlv       &aService,
                              uint16_t          aRloc16,
                              MatchMode         aMatchMode,
                              const ServiceTlv *aExcludeService,
-                             ChangedFlags &    aChangedFlags);
-    void RemoveRlocInHasRoute(PrefixTlv &      aPrefix,
-                              HasRouteTlv &    aHasRoute,
+                             ChangedFlags     &aChangedFlags);
+    void RemoveRlocInHasRoute(PrefixTlv       &aPrefix,
+                              HasRouteTlv     &aHasRoute,
                               uint16_t         aRloc16,
                               MatchMode        aMatchMode,
                               const PrefixTlv *aExcludePrefix,
-                              ChangedFlags &   aChangedFlags);
-    void RemoveRlocInBorderRouter(PrefixTlv &      aPrefix,
+                              ChangedFlags    &aChangedFlags);
+    void RemoveRlocInBorderRouter(PrefixTlv       &aPrefix,
                                   BorderRouterTlv &aBorderRouter,
                                   uint16_t         aRloc16,
                                   MatchMode        aMatchMode,
                                   const PrefixTlv *aExcludePrefix,
-                                  ChangedFlags &   aChangedFlags);
+                                  ChangedFlags    &aChangedFlags);
 
     static bool RlocMatch(uint16_t aFirstRloc16, uint16_t aSecondRloc16, MatchMode aMatchMode);
 
@@ -293,39 +321,26 @@
     UpdateStatus UpdateService(ServiceTlv &aService);
     UpdateStatus UpdateTlv(NetworkDataTlv &aTlv, const NetworkDataTlv *aSubTlvs);
 
-    static void HandleCommissioningSet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleCommissioningSet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-
-    static void HandleCommissioningGet(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleCommissioningGet(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-
-    void SendCommissioningGetResponse(const Coap::Message &   aRequest,
+    void SendCommissioningGetResponse(const Coap::Message    &aRequest,
                                       uint16_t                aLength,
                                       const Ip6::MessageInfo &aMessageInfo);
-    void SendCommissioningSetResponse(const Coap::Message &    aRequest,
-                                      const Ip6::MessageInfo & aMessageInfo,
+    void SendCommissioningSetResponse(const Coap::Message     &aRequest,
+                                      const Ip6::MessageInfo  &aMessageInfo,
                                       MeshCoP::StateTlv::State aState);
     void IncrementVersions(bool aIncludeStable);
     void IncrementVersions(const ChangedFlags &aFlags);
 
-    static constexpr uint8_t  kMinContextId        = 1;            // Minimum Context ID (0 is used for Mesh Local)
-    static constexpr uint8_t  kNumContextIds       = 15;           // Maximum Context ID
-    static constexpr uint32_t kContextIdReuseDelay = 48 * 60 * 60; // in seconds
-    static constexpr uint32_t kStateUpdatePeriod   = 60 * 1000;    // State update period in milliseconds
-    static constexpr uint32_t kMaxNetDataSyncWait  = 60 * 1000;    // Maximum time to wait for netdata sync.
+    using UpdateTimer = TimerMilliIn<Leader, &Leader::HandleTimer>;
 
-    bool       mWaitingForNetDataSync;
-    uint16_t   mContextUsed;
-    TimeMilli  mContextLastUsed[kNumContextIds];
-    uint32_t   mContextIdReuseDelay;
-    TimerMilli mTimer;
-
-    Coap::Resource mServerData;
-
-    Coap::Resource mCommissioningDataGet;
-    Coap::Resource mCommissioningDataSet;
+    bool        mWaitingForNetDataSync;
+    ContextIds  mContextIds;
+    UpdateTimer mTimer;
 };
 
+DeclareTmfHandler(Leader, kUriServerData);
+DeclareTmfHandler(Leader, kUriCommissionerGet);
+DeclareTmfHandler(Leader, kUriCommissionerSet);
+
 /**
  * @}
  */
diff --git a/src/core/thread/network_data_local.cpp b/src/core/thread/network_data_local.cpp
index 7299bef..a7eb9d9 100644
--- a/src/core/thread/network_data_local.cpp
+++ b/src/core/thread/network_data_local.cpp
@@ -168,7 +168,6 @@
 
         default:
             OT_ASSERT(false);
-            OT_UNREACHABLE_CODE(break);
         }
     }
 }
@@ -179,11 +178,11 @@
 Error Local::AddService(uint32_t           aEnterpriseNumber,
                         const ServiceData &aServiceData,
                         bool               aServerStable,
-                        const ServerData & aServerData)
+                        const ServerData  &aServerData)
 {
     Error       error = kErrorNone;
     ServiceTlv *serviceTlv;
-    ServerTlv * serverTlv;
+    ServerTlv  *serverTlv;
     uint16_t    serviceTlvSize = ServiceTlv::CalculateSize(aEnterpriseNumber, aServiceData.GetLength()) +
                               sizeof(ServerTlv) + aServerData.GetLength();
 
@@ -243,7 +242,6 @@
 
         default:
             OT_ASSERT(false);
-            OT_UNREACHABLE_CODE(break);
         }
     }
 }
@@ -271,46 +269,10 @@
 
         default:
             OT_ASSERT(false);
-            OT_UNREACHABLE_CODE(break);
         }
     }
 }
 
-bool Local::IsConsistent(void) const
-{
-    return Get<Leader>().ContainsEntriesFrom(*this, Get<Mle::MleRouter>().GetRloc16()) &&
-           ContainsEntriesFrom(Get<Leader>(), Get<Mle::MleRouter>().GetRloc16());
-}
-
-Error Local::UpdateInconsistentServerData(Coap::ResponseHandler aHandler, void *aContext)
-{
-    Error    error = kErrorNone;
-    uint16_t rloc  = Get<Mle::MleRouter>().GetRloc16();
-
-#if OPENTHREAD_FTD
-    // Don't send this Server Data Notification if the device is going to upgrade to Router
-    if (Get<Mle::MleRouter>().IsExpectedToBecomeRouterSoon())
-    {
-        ExitNow(error = kErrorInvalidState);
-    }
-#endif
-
-    UpdateRloc();
-
-    VerifyOrExit(!IsConsistent(), error = kErrorNotFound);
-
-    if (mOldRloc == rloc)
-    {
-        mOldRloc = Mac::kShortAddrInvalid;
-    }
-
-    SuccessOrExit(error = SendServerDataNotification(mOldRloc, /* aAppendNetDataTlv */ true, aHandler, aContext));
-    mOldRloc = rloc;
-
-exit:
-    return error;
-}
-
 } // namespace NetworkData
 } // namespace ot
 
diff --git a/src/core/thread/network_data_local.hpp b/src/core/thread/network_data_local.hpp
index ec274d1..27090a5 100644
--- a/src/core/thread/network_data_local.hpp
+++ b/src/core/thread/network_data_local.hpp
@@ -54,12 +54,16 @@
 
 namespace NetworkData {
 
+class Notifier;
+
 /**
  * This class implements the Thread Network Data contributed by the local device.
  *
  */
 class Local : public MutableNetworkData, private NonCopyable
 {
+    friend class Notifier;
+
 public:
     /**
      * This constructor initializes the local Network Data.
@@ -69,7 +73,6 @@
      */
     explicit Local(Instance &aInstance)
         : MutableNetworkData(aInstance, mTlvBuffer, 0, sizeof(mTlvBuffer))
-        , mOldRloc(Mac::kShortAddrInvalid)
     {
     }
 
@@ -148,7 +151,7 @@
     Error AddService(uint32_t           aEnterpriseNumber,
                      const ServiceData &aServiceData,
                      bool               aServerStable,
-                     const ServerData & aServerData);
+                     const ServerData  &aServerData);
 
     /**
      * This method removes a Service entry from the Thread Network local data.
@@ -163,23 +166,8 @@
     Error RemoveService(uint32_t aEnterpriseNumber, const ServiceData &aServiceData);
 #endif // OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
 
-    /**
-     * This method sends a Server Data Notification message to the Leader.
-     *
-     * @param[in]  aHandler  A function pointer that is called when the transaction ends.
-     * @param[in]  aContext  A pointer to arbitrary context information.
-     *
-     * @retval kErrorNone          Successfully enqueued the notification message.
-     * @retval kErrorNoBufs        Insufficient message buffers to generate the notification message.
-     * @retval kErrorInvalidState  Device is a REED and is in the process of becoming a Router.
-     * @retval kErrorNotFound      Server Data is already consistent with network data.
-     *
-     */
-    Error UpdateInconsistentServerData(Coap::ResponseHandler aHandler, void *aContext);
-
 private:
     void UpdateRloc(void);
-    bool IsConsistent(void) const;
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
     Error AddPrefix(const Ip6::Prefix &aPrefix, NetworkDataTlv::Type aSubTlvType, uint16_t aFlags, bool aStable);
@@ -191,8 +179,7 @@
     void UpdateRloc(ServiceTlv &aService);
 #endif
 
-    uint8_t  mTlvBuffer[kMaxSize];
-    uint16_t mOldRloc;
+    uint8_t mTlvBuffer[kMaxSize];
 };
 
 } // namespace NetworkData
diff --git a/src/core/thread/network_data_notifier.cpp b/src/core/thread/network_data_notifier.cpp
index 00b15ce..65b76e0 100644
--- a/src/core/thread/network_data_notifier.cpp
+++ b/src/core/thread/network_data_notifier.cpp
@@ -41,6 +41,8 @@
 #include "common/log.hpp"
 #include "thread/network_data_leader.hpp"
 #include "thread/network_data_local.hpp"
+#include "thread/tmf.hpp"
+#include "thread/uri_paths.hpp"
 
 namespace ot {
 namespace NetworkData {
@@ -49,9 +51,10 @@
 
 Notifier::Notifier(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mTimer(aInstance, HandleTimer)
-    , mSynchronizeDataTask(aInstance, HandleSynchronizeDataTask)
+    , mTimer(aInstance)
+    , mSynchronizeDataTask(aInstance)
     , mNextDelay(0)
+    , mOldRloc(Mac::kShortAddrInvalid)
     , mWaitingForResponse(false)
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTER_REQUEST_ROUTER_ROLE
     , mDidRequestRouterRoleUpgrade(false)
@@ -71,11 +74,6 @@
     mSynchronizeDataTask.Post();
 }
 
-void Notifier::HandleSynchronizeDataTask(Tasklet &aTasklet)
-{
-    aTasklet.Get<Notifier>().SynchronizeServerData();
-}
-
 void Notifier::SynchronizeServerData(void)
 {
     Error error = kErrorNotFound;
@@ -86,13 +84,13 @@
 
 #if OPENTHREAD_FTD
     mNextDelay = kDelayRemoveStaleChildren;
-    error      = Get<Leader>().RemoveStaleChildEntries(&Notifier::HandleCoapResponse, this);
+    error      = RemoveStaleChildEntries();
     VerifyOrExit(error == kErrorNotFound);
 #endif
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE || OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
     mNextDelay = kDelaySynchronizeServerData;
-    error      = Get<Local>().UpdateInconsistentServerData(&Notifier::HandleCoapResponse, this);
+    error      = UpdateInconsistentData();
     VerifyOrExit(error == kErrorNotFound);
 #endif
 
@@ -114,10 +112,112 @@
         break;
     default:
         OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
     }
 }
 
+#if OPENTHREAD_FTD
+Error Notifier::RemoveStaleChildEntries(void)
+{
+    // Check if there is any stale child entry in network data and send
+    // a "Server Data" notification to leader to remove it.
+    //
+    // - `kErrorNone` when a stale child entry was found and successfully
+    //    sent a "Server Data" notification to leader.
+    // - `kErrorNoBufs` if could not allocate message to send message.
+    // - `kErrorNotFound` if no stale child entries were found.
+
+    Error    error    = kErrorNotFound;
+    Iterator iterator = kIteratorInit;
+    uint16_t rloc16;
+
+    VerifyOrExit(Get<Mle::MleRouter>().IsRouterOrLeader());
+
+    while (Get<Leader>().GetNextServer(iterator, rloc16) == kErrorNone)
+    {
+        if (!Mle::IsActiveRouter(rloc16) && Mle::RouterIdMatch(Get<Mle::MleRouter>().GetRloc16(), rloc16) &&
+            Get<ChildTable>().FindChild(rloc16, Child::kInStateValid) == nullptr)
+        {
+            error = SendServerDataNotification(rloc16);
+            ExitNow();
+        }
+    }
+
+exit:
+    return error;
+}
+#endif // OPENTHREAD_FTD
+
+#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE || OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
+Error Notifier::UpdateInconsistentData(void)
+{
+    Error    error      = kErrorNone;
+    uint16_t deviceRloc = Get<Mle::MleRouter>().GetRloc16();
+
+#if OPENTHREAD_FTD
+    // Don't send this Server Data Notification if the device is going
+    // to upgrade to Router.
+
+    if (Get<Mle::MleRouter>().IsExpectedToBecomeRouterSoon())
+    {
+        ExitNow(error = kErrorInvalidState);
+    }
+#endif
+
+    Get<Local>().UpdateRloc();
+
+    if (Get<Leader>().ContainsEntriesFrom(Get<Local>(), deviceRloc) &&
+        Get<Local>().ContainsEntriesFrom(Get<Leader>(), deviceRloc))
+    {
+        ExitNow(error = kErrorNotFound);
+    }
+
+    if (mOldRloc == deviceRloc)
+    {
+        mOldRloc = Mac::kShortAddrInvalid;
+    }
+
+    SuccessOrExit(error = SendServerDataNotification(mOldRloc, &Get<Local>()));
+    mOldRloc = deviceRloc;
+
+exit:
+    return error;
+}
+#endif // #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE || OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
+
+Error Notifier::SendServerDataNotification(uint16_t aOldRloc16, const NetworkData *aNetworkData)
+{
+    Error            error = kErrorNone;
+    Coap::Message   *message;
+    Tmf::MessageInfo messageInfo(GetInstance());
+
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriServerData);
+    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
+
+    if (aNetworkData != nullptr)
+    {
+        ThreadTlv tlv;
+
+        tlv.SetType(ThreadTlv::kThreadNetworkData);
+        tlv.SetLength(aNetworkData->GetLength());
+        SuccessOrExit(error = message->Append(tlv));
+        SuccessOrExit(error = message->AppendBytes(aNetworkData->GetBytes(), aNetworkData->GetLength()));
+    }
+
+    if (aOldRloc16 != Mac::kShortAddrInvalid)
+    {
+        SuccessOrExit(error = Tlv::Append<ThreadRloc16Tlv>(*message, aOldRloc16));
+    }
+
+    IgnoreError(messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc());
+    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, HandleCoapResponse, this));
+
+    LogInfo("Sent %s", UriToString<kUriServerData>());
+
+exit:
+    FreeMessageOnError(message, error);
+    return error;
+}
+
 void Notifier::HandleNotifierEvents(Events aEvents)
 {
     if (aEvents.ContainsAny(kEventThreadRoleChanged | kEventThreadChildRemoved))
@@ -143,15 +243,7 @@
     }
 }
 
-void Notifier::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Notifier>().HandleTimer();
-}
-
-void Notifier::HandleTimer(void)
-{
-    SynchronizeServerData();
-}
+void Notifier::HandleTimer(void) { SynchronizeServerData(); }
 
 void Notifier::HandleCoapResponse(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo, Error aResult)
 {
@@ -178,7 +270,6 @@
 
     default:
         OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
     }
 }
 
diff --git a/src/core/thread/network_data_notifier.hpp b/src/core/thread/network_data_notifier.hpp
index 2388da7..65426cd 100644
--- a/src/core/thread/network_data_notifier.hpp
+++ b/src/core/thread/network_data_notifier.hpp
@@ -48,6 +48,8 @@
 namespace ot {
 namespace NetworkData {
 
+class NetworkData;
+
 /**
  * This class implements the SVR_DATA.ntf transmission logic.
  *
@@ -104,29 +106,36 @@
     static constexpr uint32_t kDelaySynchronizeServerData  = 300000; // in msec
     static constexpr uint8_t  kRouterRoleUpgradeMaxTimeout = 10;     // in sec
 
-    void HandleNotifierEvents(Events aEvents);
+    void  SynchronizeServerData(void);
+    Error SendServerDataNotification(uint16_t aOldRloc16, const NetworkData *aNetworkData = nullptr);
+#if OPENTHREAD_FTD
+    Error RemoveStaleChildEntries(void);
+#endif
+#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE || OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
+    Error UpdateInconsistentData(void);
+#endif
 
-    static void HandleTimer(Timer &aTimer);
+    void        HandleNotifierEvents(Events aEvents);
     void        HandleTimer(void);
-
-    static void HandleCoapResponse(void *               aContext,
-                                   otMessage *          aMessage,
+    static void HandleCoapResponse(void                *aContext,
+                                   otMessage           *aMessage,
                                    const otMessageInfo *aMessageInfo,
                                    Error                aResult);
     void        HandleCoapResponse(Error aResult);
 
-    static void HandleSynchronizeDataTask(Tasklet &aTasklet);
-
-    void SynchronizeServerData(void);
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTER_REQUEST_ROUTER_ROLE
     void ScheduleRouterRoleUpgradeIfEligible(void);
     void HandleTimeTick(void);
 #endif
 
-    TimerMilli mTimer;
-    Tasklet    mSynchronizeDataTask;
-    uint32_t   mNextDelay;
-    bool       mWaitingForResponse : 1;
+    using SynchronizeDataTask = TaskletIn<Notifier, &Notifier::SynchronizeServerData>;
+    using DelayTimer          = TimerMilliIn<Notifier, &Notifier::HandleTimer>;
+
+    DelayTimer          mTimer;
+    SynchronizeDataTask mSynchronizeDataTask;
+    uint32_t            mNextDelay;
+    uint16_t            mOldRloc;
+    bool                mWaitingForResponse : 1;
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTER_REQUEST_ROUTER_ROLE
     bool    mDidRequestRouterRoleUpgrade : 1;
diff --git a/src/core/thread/network_data_publisher.cpp b/src/core/thread/network_data_publisher.cpp
index 8141c76..056765c 100644
--- a/src/core/thread/network_data_publisher.cpp
+++ b/src/core/thread/network_data_publisher.cpp
@@ -59,17 +59,13 @@
 #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
     , mDnsSrpServiceEntry(aInstance)
 #endif
-#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
-    , mPrefixCallback(nullptr)
-    , mPrefixCallbackContext(nullptr)
-#endif
-    , mTimer(aInstance, Publisher::HandleTimer)
+    , mTimer(aInstance)
 {
 #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)
@@ -81,13 +77,7 @@
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
 
-void Publisher::SetPrefixCallback(PrefixCallback aCallback, void *aContext)
-{
-    mPrefixCallback        = aCallback;
-    mPrefixCallbackContext = aContext;
-}
-
-Error Publisher::PublishOnMeshPrefix(const OnMeshPrefixConfig &aConfig)
+Error Publisher::PublishOnMeshPrefix(const OnMeshPrefixConfig &aConfig, Requester aRequester)
 {
     Error        error = kErrorNone;
     PrefixEntry *entry;
@@ -95,16 +85,23 @@
     VerifyOrExit(aConfig.IsValid(GetInstance()), error = kErrorInvalidArgs);
     VerifyOrExit(aConfig.mStable, error = kErrorInvalidArgs);
 
-    entry = FindOrAllocatePrefixEntry(aConfig.GetPrefix());
+    entry = FindOrAllocatePrefixEntry(aConfig.GetPrefix(), aRequester);
     VerifyOrExit(entry != nullptr, error = kErrorNoBufs);
 
-    entry->Publish(aConfig);
+    entry->Publish(aConfig, aRequester);
 
 exit:
     return error;
 }
 
-Error Publisher::PublishExternalRoute(const ExternalRouteConfig &aConfig)
+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;
@@ -112,10 +109,10 @@
     VerifyOrExit(aConfig.IsValid(GetInstance()), error = kErrorInvalidArgs);
     VerifyOrExit(aConfig.mStable, error = kErrorInvalidArgs);
 
-    entry = FindOrAllocatePrefixEntry(aConfig.GetPrefix());
+    entry = FindOrAllocatePrefixEntry(aPrefix, aRequester);
     VerifyOrExit(entry != nullptr, error = kErrorNoBufs);
 
-    entry->Publish(aConfig);
+    entry->Publish(aConfig, aRequester);
 
 exit:
     return error;
@@ -149,24 +146,48 @@
     return error;
 }
 
-Publisher::PrefixEntry *Publisher::FindOrAllocatePrefixEntry(const Ip6::Prefix &aPrefix)
+Publisher::PrefixEntry *Publisher::FindOrAllocatePrefixEntry(const Ip6::Prefix &aPrefix, Requester aRequester)
 {
     // Returns a matching prefix entry if found, otherwise tries
     // to allocate a new entry.
 
-    PrefixEntry *prefixEntry = FindMatchingPrefixEntry(aPrefix);
-
-    VerifyOrExit(prefixEntry == nullptr);
+    PrefixEntry *prefixEntry = nullptr;
+    uint16_t     numEntries  = 0;
+    uint8_t      maxEntries  = 0;
 
     for (PrefixEntry &entry : mPrefixEntries)
     {
-        if (!entry.IsInUse())
+        if (entry.IsInUse())
+        {
+            if (entry.GetRequester() == aRequester)
+            {
+                numEntries++;
+            }
+
+            if (entry.Matches(aPrefix))
+            {
+                prefixEntry = &entry;
+                ExitNow();
+            }
+        }
+        else if (prefixEntry == nullptr)
         {
             prefixEntry = &entry;
-            ExitNow();
         }
     }
 
+    switch (aRequester)
+    {
+    case kFromUser:
+        maxEntries = kMaxUserPrefixEntries;
+        break;
+    case kFromRoutingManager:
+        maxEntries = kMaxRoutingManagerPrefixEntries;
+        break;
+    }
+
+    VerifyOrExit(numEntries < maxEntries, prefixEntry = nullptr);
+
 exit:
     return prefixEntry;
 }
@@ -199,10 +220,7 @@
 
 void Publisher::NotifyPrefixEntryChange(Event aEvent, const Ip6::Prefix &aPrefix) const
 {
-    if (mPrefixCallback != nullptr)
-    {
-        mPrefixCallback(static_cast<otNetDataPublisherEvent>(aEvent), &aPrefix, mPrefixCallbackContext);
-    }
+    mPrefixCallback.InvokeIfSet(static_cast<otNetDataPublisherEvent>(aEvent), &aPrefix);
 }
 
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
@@ -221,11 +239,6 @@
 #endif
 }
 
-void Publisher::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<Publisher>().HandleTimer();
-}
-
 void Publisher::HandleTimer(void)
 {
 #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
@@ -262,7 +275,7 @@
     // router over an entry from an end-device (e.g., a REED). If both
     // are the same type, then the one with smaller RLOC16 is preferred.
 
-    bool isOtherRouter = Mle::Mle::IsActiveRouter(aRloc16);
+    bool isOtherRouter = Mle::IsActiveRouter(aRloc16);
 
     return (Get<Mle::Mle>().IsRouterOrLeader() == isOtherRouter) ? (aRloc16 < Get<Mle::Mle>().GetRloc16())
                                                                  : isOtherRouter;
@@ -324,7 +337,7 @@
 
             if (aNumPreferredEntries < aDesiredNumEntries)
             {
-                mUpdateTime += kExtraDelayToRemovePeferred;
+                mUpdateTime += kExtraDelayToRemovePreferred;
             }
 
             SetState(kRemoving);
@@ -437,7 +450,7 @@
             break;
         }
 
-        string.Append(prefixEntry.mPrefix.ToString().AsCString());
+        string.Append("%s", prefixEntry.mPrefix.ToString().AsCString());
         ExitNow();
     }
 #endif
@@ -453,7 +466,7 @@
 
 void Publisher::Entry::LogUpdateTime(void) const
 {
-    LogInfo("%s - update in %u msec", ToString().AsCString(), mUpdateTime - TimerMilli::GetNow());
+    LogInfo("%s - update in %lu msec", ToString().AsCString(), ToUlong(mUpdateTime - TimerMilli::GetNow()));
 }
 
 const char *Publisher::Entry::StateToString(State aState)
@@ -480,18 +493,7 @@
 //---------------------------------------------------------------------------------------------------------------------
 // Publisher::DnsSrpServiceEntry
 
-Publisher::DnsSrpServiceEntry::DnsSrpServiceEntry(Instance &aInstance)
-    : mCallback(nullptr)
-    , mCallbackContext(nullptr)
-{
-    Init(aInstance);
-}
-
-void Publisher::DnsSrpServiceEntry::SetCallback(DnsSrpServiceCallback aCallback, void *aContext)
-{
-    mCallback        = aCallback;
-    mCallbackContext = aContext;
-}
+Publisher::DnsSrpServiceEntry::DnsSrpServiceEntry(Instance &aInstance) { Init(aInstance); }
 
 void Publisher::DnsSrpServiceEntry::PublishAnycast(uint8_t aSequenceNumber)
 {
@@ -630,10 +632,7 @@
     Get<Srp::Server>().HandleNetDataPublisherEvent(aEvent);
 #endif
 
-    if (mCallback != nullptr)
-    {
-        mCallback(static_cast<otNetDataPublisherEvent>(aEvent), mCallbackContext);
-    }
+    mCallback.InvokeIfSet(static_cast<otNetDataPublisherEvent>(aEvent));
 }
 
 void Publisher::DnsSrpServiceEntry::Process(void)
@@ -680,7 +679,7 @@
     // smaller RLCO16.
 
     Service::DnsSrpAnycast::ServiceData serviceData(mInfo.GetSequenceNumber());
-    const ServiceTlv *                  serviceTlv = nullptr;
+    const ServiceTlv                   *serviceTlv = nullptr;
     ServiceData                         data;
 
     data.Init(&serviceData, serviceData.GetLength());
@@ -791,41 +790,47 @@
 //---------------------------------------------------------------------------------------------------------------------
 // Publisher::PrefixEntry
 
-void Publisher::PrefixEntry::Publish(const OnMeshPrefixConfig &aConfig)
+void Publisher::PrefixEntry::Publish(const OnMeshPrefixConfig &aConfig, Requester aRequester)
 {
     LogInfo("Publishing OnMeshPrefix %s", aConfig.GetPrefix().ToString().AsCString());
 
-    Publish(aConfig.GetPrefix(), aConfig.ConvertToTlvFlags(), kTypeOnMeshPrefix);
+    Publish(aConfig.GetPrefix(), aConfig.ConvertToTlvFlags(), kTypeOnMeshPrefix, aRequester);
 }
 
-void Publisher::PrefixEntry::Publish(const ExternalRouteConfig &aConfig)
+void Publisher::PrefixEntry::Publish(const ExternalRouteConfig &aConfig, Requester aRequester)
 {
     LogInfo("Publishing ExternalRoute %s", aConfig.GetPrefix().ToString().AsCString());
 
-    Publish(aConfig.GetPrefix(), aConfig.ConvertToTlvFlags(), kTypeExternalRoute);
+    Publish(aConfig.GetPrefix(), aConfig.ConvertToTlvFlags(), kTypeExternalRoute, aRequester);
 }
 
-void Publisher::PrefixEntry::Publish(const Ip6::Prefix &aPrefix, uint16_t aNewFlags, Type aNewType)
+void Publisher::PrefixEntry::Publish(const Ip6::Prefix &aPrefix,
+                                     uint16_t           aNewFlags,
+                                     Type               aNewType,
+                                     Requester          aRequester)
 {
+    mRequester = aRequester;
+
     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();
         }
     }
@@ -962,7 +967,7 @@
 
 void Publisher::PrefixEntry::CountOnMeshPrefixEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const
 {
-    const PrefixTlv *      prefixTlv;
+    const PrefixTlv       *prefixTlv;
     const BorderRouterTlv *brSubTlv;
     int8_t                 preference             = BorderRouterEntry::PreferenceFromFlags(mFlags);
     uint16_t               flagsWithoutPreference = BorderRouterEntry::FlagsWithoutPreference(mFlags);
@@ -1009,7 +1014,7 @@
 
 void Publisher::PrefixEntry::CountExternalRouteEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const
 {
-    const PrefixTlv *  prefixTlv;
+    const PrefixTlv   *prefixTlv;
     const HasRouteTlv *hrSubTlv;
     int8_t             preference             = HasRouteEntry::PreferenceFromFlags(static_cast<uint8_t>(mFlags));
     uint8_t            flagsWithoutPreference = HasRouteEntry::FlagsWithoutPreference(static_cast<uint8_t>(mFlags));
diff --git a/src/core/thread/network_data_publisher.hpp b/src/core/thread/network_data_publisher.hpp
index 7d11790..3d3a37f 100644
--- a/src/core/thread/network_data_publisher.hpp
+++ b/src/core/thread/network_data_publisher.hpp
@@ -43,14 +43,10 @@
             "or OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE"
 #endif
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && (OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_PREFIX_ENTRIES < \
-                                                (OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_PREFIXES + 4))
-#error "OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_PREFIX_ENTRIES needs to support more entries when "\
-       "OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE is enabled to accommodate for max on-link prefixes"
-#endif
-
 #include <openthread/netdata_publisher.h>
 
+#include "border_router/routing_manager.hpp"
+#include "common/callback.hpp"
 #include "common/clearable.hpp"
 #include "common/equatable.hpp"
 #include "common/error.hpp"
@@ -88,6 +84,16 @@
     };
 
     /**
+     * This enumeration represents the requester associated with a published prefix.
+     *
+     */
+    enum Requester : uint8_t
+    {
+        kFromUser,           ///< Requested by user (public OT API).
+        kFromRoutingManager, ///< Requested by `RoutingManager` module.
+    };
+
+    /**
      * This constructor initializes `Publisher` object.
      *
      * @param[in]  aInstance     A reference to the OpenThread instance.
@@ -207,7 +213,7 @@
      * @param[in] aContext         A pointer to application-specific context (used when @p aCallback is invoked).
      *
      */
-    void SetPrefixCallback(PrefixCallback aCallback, void *aContext);
+    void SetPrefixCallback(PrefixCallback aCallback, void *aContext) { mPrefixCallback.Set(aCallback, aContext); }
 
     /**
      * This method requests an on-mesh prefix to be published in the Thread Network Data.
@@ -222,6 +228,7 @@
      * same prefix with the same or higher preference.
      *
      * @param[in] aConfig         The on-mesh prefix config to publish.
+     * @param[in] aRequester      The requester (`kFromUser` or `kFromRoutingManager` module).
      *
      * @retval kErrorNone         The on-mesh prefix is published successfully.
      * @retval kErrorInvalidArgs  The @p aConfig is not valid (bad prefix, invalid flag combinations, or not stable).
@@ -232,7 +239,7 @@
      *
      *
      */
-    Error PublishOnMeshPrefix(const OnMeshPrefixConfig &aConfig);
+    Error PublishOnMeshPrefix(const OnMeshPrefixConfig &aConfig, Requester aRequester);
 
     /**
      * This method requests an external route prefix to be published in the Thread Network Data.
@@ -247,6 +254,7 @@
      * same prefix with the same or higher preference.
      *
      * @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).
@@ -256,7 +264,44 @@
      *
      *
      */
-    Error PublishExternalRoute(const ExternalRouteConfig &aConfig);
+    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
@@ -298,10 +343,10 @@
         // 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 = 50;
+        static constexpr uint16_t kInfoStringSize = 60;
 
         typedef String<kInfoStringSize> InfoString;
 
@@ -339,7 +384,7 @@
 
     public:
         explicit DnsSrpServiceEntry(Instance &aInstance);
-        void SetCallback(DnsSrpServiceCallback aCallback, void *aContext);
+        void SetCallback(DnsSrpServiceCallback aCallback, void *aContext) { mCallback.Set(aCallback, aContext); }
         void PublishAnycast(uint8_t aSequenceNumber);
         void PublishUnicast(const Ip6::Address &aAddress, uint16_t aPort);
         void PublishUnicast(uint16_t aPort);
@@ -394,29 +439,36 @@
         void CountAnycastEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const;
         void CountUnicastEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const;
 
-        Info                  mInfo;
-        DnsSrpServiceCallback mCallback;
-        void *                mCallbackContext;
+        Info                            mInfo;
+        Callback<DnsSrpServiceCallback> mCallback;
     };
 #endif // OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
+
     // Max number of prefix (on-mesh or external route) entries.
-    static constexpr uint16_t kMaxPrefixEntries = OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_PREFIX_ENTRIES;
+    static constexpr uint16_t kMaxUserPrefixEntries = OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_PREFIX_ENTRIES;
+
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    static constexpr uint16_t kMaxRoutingManagerPrefixEntries = BorderRouter::RoutingManager::kMaxPublishedPrefixes;
+#else
+    static constexpr uint16_t kMaxRoutingManagerPrefixEntries = 0;
+#endif
 
     class PrefixEntry : public Entry, private NonCopyable
     {
         friend class Entry;
 
     public:
-        void Init(Instance &aInstance) { Entry::Init(aInstance); }
-        bool IsInUse(void) const { return GetState() != kNoEntry; }
-        bool Matches(const Ip6::Prefix &aPrefix) const { return mPrefix == aPrefix; }
-        void Publish(const OnMeshPrefixConfig &aConfig);
-        void Publish(const ExternalRouteConfig &aConfig);
-        void Unpublish(void);
-        void HandleTimer(void) { Entry::HandleTimer(); }
-        void HandleNotifierEvents(Events aEvents);
+        void      Init(Instance &aInstance) { Entry::Init(aInstance); }
+        bool      IsInUse(void) const { return GetState() != kNoEntry; }
+        bool      Matches(const Ip6::Prefix &aPrefix) const { return mPrefix == aPrefix; }
+        void      Publish(const OnMeshPrefixConfig &aConfig, Requester aRequester);
+        void      Publish(const ExternalRouteConfig &aConfig, Requester aRequester);
+        Requester GetRequester(void) const { return mRequester; }
+        void      Unpublish(void);
+        void      HandleTimer(void) { Entry::HandleTimer(); }
+        void      HandleNotifierEvents(Events aEvents);
 
     private:
         static constexpr uint8_t kDesiredNumOnMeshPrefix =
@@ -431,7 +483,7 @@
             kTypeExternalRoute,
         };
 
-        void  Publish(const Ip6::Prefix &aPrefix, uint16_t aNewFlags, Type aNewType);
+        void  Publish(const Ip6::Prefix &aPrefix, uint16_t aNewFlags, Type aNewType, Requester aRequester);
         void  Add(void);
         Error AddOnMeshPrefix(void);
         Error AddExternalRoute(void);
@@ -441,6 +493,7 @@
         void  CountExternalRouteEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const;
 
         Type        mType;
+        Requester   mRequester;
         Ip6::Prefix mPrefix;
         uint16_t    mFlags;
     };
@@ -451,8 +504,8 @@
 #endif
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
-    PrefixEntry *      FindOrAllocatePrefixEntry(const Ip6::Prefix &aPrefix);
-    PrefixEntry *      FindMatchingPrefixEntry(const Ip6::Prefix &aPrefix);
+    PrefixEntry       *FindOrAllocatePrefixEntry(const Ip6::Prefix &aPrefix, Requester aRequester);
+    PrefixEntry       *FindMatchingPrefixEntry(const Ip6::Prefix &aPrefix);
     const PrefixEntry *FindMatchingPrefixEntry(const Ip6::Prefix &aPrefix) const;
     bool               IsAPrefixEntry(const Entry &aEntry) const;
     void               NotifyPrefixEntryChange(Event aEvent, const Ip6::Prefix &aPrefix) const;
@@ -460,20 +513,20 @@
 
     TimerMilli &GetTimer(void) { return mTimer; }
     void        HandleNotifierEvents(Events aEvents);
-    static void HandleTimer(Timer &aTimer);
     void        HandleTimer(void);
 
+    using PublisherTimer = TimerMilliIn<Publisher, &Publisher::HandleTimer>;
+
 #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
     DnsSrpServiceEntry mDnsSrpServiceEntry;
 #endif
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
-    PrefixEntry    mPrefixEntries[kMaxPrefixEntries];
-    PrefixCallback mPrefixCallback;
-    void *         mPrefixCallbackContext;
+    PrefixEntry              mPrefixEntries[kMaxUserPrefixEntries + kMaxRoutingManagerPrefixEntries];
+    Callback<PrefixCallback> mPrefixCallback;
 #endif
 
-    TimerMilli mTimer;
+    PublisherTimer mTimer;
 };
 
 } // namespace NetworkData
diff --git a/src/core/thread/network_data_service.cpp b/src/core/thread/network_data_service.cpp
index 618812b..1d2e90d 100644
--- a/src/core/thread/network_data_service.cpp
+++ b/src/core/thread/network_data_service.cpp
@@ -83,7 +83,7 @@
 Error Manager::GetServiceId(const void *aServiceData,
                             uint8_t     aServiceDataLength,
                             bool        aServerStable,
-                            uint8_t &   aServiceId) const
+                            uint8_t    &aServiceId) const
 {
     ServiceData serviceData;
 
@@ -94,11 +94,11 @@
 
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
 
-void Manager::GetBackboneRouterPrimary(ot::BackboneRouter::BackboneRouterConfig &aConfig) const
+void Manager::GetBackboneRouterPrimary(ot::BackboneRouter::Config &aConfig) const
 {
-    const ServerTlv *                 rvalServerTlv  = nullptr;
+    const ServerTlv                  *rvalServerTlv  = nullptr;
     const BackboneRouter::ServerData *rvalServerData = nullptr;
-    const ServiceTlv *                serviceTlv     = nullptr;
+    const ServiceTlv                 *serviceTlv     = nullptr;
     ServiceData                       serviceData;
 
     serviceData.Init(&BackboneRouter::kServiceData, BackboneRouter::kServiceDataMinSize);
@@ -146,13 +146,13 @@
     return;
 }
 
-bool Manager::IsBackboneRouterPreferredTo(const ServerTlv &                 aServerTlv,
+bool Manager::IsBackboneRouterPreferredTo(const ServerTlv                  &aServerTlv,
                                           const BackboneRouter::ServerData &aServerData,
-                                          const ServerTlv &                 aOtherServerTlv,
+                                          const ServerTlv                  &aOtherServerTlv,
                                           const BackboneRouter::ServerData &aOtherServerData) const
 {
     bool     isPreferred;
-    uint16_t leaderRloc16 = Mle::Mle::Rloc16FromRouterId(Get<Mle::MleRouter>().GetLeaderId());
+    uint16_t leaderRloc16 = Mle::Rloc16FromRouterId(Get<Mle::MleRouter>().GetLeaderId());
 
     VerifyOrExit(aServerTlv.GetServer16() != leaderRloc16, isPreferred = true);
     VerifyOrExit(aOtherServerTlv.GetServer16() != leaderRloc16, isPreferred = false);
@@ -183,7 +183,7 @@
 
     tlv->GetServiceData(serviceData);
     aInfo.mAnycastAddress.SetToAnycastLocator(Get<Mle::Mle>().GetMeshLocalPrefix(),
-                                              Mle::Mle::ServiceAlocFromId(tlv->GetServiceId()));
+                                              Mle::ServiceAlocFromId(tlv->GetServiceId()));
     aInfo.mSequenceNumber =
         reinterpret_cast<const DnsSrpAnycast::ServiceData *>(serviceData.GetBytes())->GetSequenceNumber();
 
@@ -288,6 +288,7 @@
                 aInfo.mSockAddr.SetAddress(serverData->GetAddress());
                 aInfo.mSockAddr.SetPort(serverData->GetPort());
                 aInfo.mOrigin = DnsSrpUnicast::kFromServerData;
+                aInfo.mRloc16 = aIterator.mServerSubTlv->GetServer16();
                 ExitNow();
             }
 
@@ -300,6 +301,7 @@
                                                                  aIterator.mServerSubTlv->GetServer16());
                 aInfo.mSockAddr.SetPort(Encoding::BigEndian::ReadUint16(data.GetBytes()));
                 aInfo.mOrigin = DnsSrpUnicast::kFromServerData;
+                aInfo.mRloc16 = aIterator.mServerSubTlv->GetServer16();
                 ExitNow();
             }
         }
@@ -322,6 +324,7 @@
             aInfo.mSockAddr.SetAddress(dnsServiceData->GetAddress());
             aInfo.mSockAddr.SetPort(dnsServiceData->GetPort());
             aInfo.mOrigin = DnsSrpUnicast::kFromServiceData;
+            aInfo.mRloc16 = Mle::kInvalidRloc16;
             ExitNow();
         }
 
diff --git a/src/core/thread/network_data_service.hpp b/src/core/thread/network_data_service.hpp
index bd41dc7..5dec83a 100644
--- a/src/core/thread/network_data_service.hpp
+++ b/src/core/thread/network_data_service.hpp
@@ -258,6 +258,7 @@
     {
         Ip6::SockAddr mSockAddr; ///< The socket address (IPv6 address and port) of the DNS/SRP server.
         Origin        mOrigin;   ///< The origin of the socket address (whether from service or server data).
+        uint16_t      mRloc16;   ///< The BR RLOC16 adding the entry (only used when `mOrigin == kFromServerData`).
     };
 
     /**
@@ -404,7 +405,7 @@
 
     private:
         const ServiceTlv *mServiceTlv;
-        const ServerTlv * mServerSubTlv;
+        const ServerTlv  *mServerSubTlv;
     };
 
     /**
@@ -422,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:
@@ -449,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:
@@ -494,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).
@@ -543,7 +544,7 @@
      * @param[out]  aConfig      The Primary Backbone Router configuration.
      *
      */
-    void GetBackboneRouterPrimary(ot::BackboneRouter::BackboneRouterConfig &aConfig) const;
+    void GetBackboneRouterPrimary(ot::BackboneRouter::Config &aConfig) const;
 #endif
 
     /**
@@ -604,13 +605,13 @@
     Error GetServiceId(const void *aServiceData,
                        uint8_t     aServiceDataLength,
                        bool        aServerStable,
-                       uint8_t &   aServiceId) const;
+                       uint8_t    &aServiceId) const;
     Error IterateToNextServer(Iterator &aIterator) const;
 
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
-    bool IsBackboneRouterPreferredTo(const ServerTlv &                 aServerTlv,
+    bool IsBackboneRouterPreferredTo(const ServerTlv                  &aServerTlv,
                                      const BackboneRouter::ServerData &aServerData,
-                                     const ServerTlv &                 aOtherServerTlv,
+                                     const ServerTlv                  &aOtherServerTlv,
                                      const BackboneRouter::ServerData &aOtherServerData) const;
 #endif
 };
diff --git a/src/core/thread/network_data_tlvs.cpp b/src/core/thread/network_data_tlvs.cpp
index 6d2d400..e81ec2d 100644
--- a/src/core/thread/network_data_tlvs.cpp
+++ b/src/core/thread/network_data_tlvs.cpp
@@ -81,10 +81,7 @@
 //---------------------------------------------------------------------------------------------------------------------
 // PrefixTlv
 
-const NetworkDataTlv *PrefixTlv::FindSubTlv(Type aType) const
-{
-    return Find(GetSubTlvs(), GetNext(), aType);
-}
+const NetworkDataTlv *PrefixTlv::FindSubTlv(Type aType) const { return Find(GetSubTlvs(), GetNext(), aType); }
 
 const NetworkDataTlv *PrefixTlv::FindSubTlv(Type aType, bool aStable) const
 {
diff --git a/src/core/thread/network_data_tlvs.hpp b/src/core/thread/network_data_tlvs.hpp
index 48568c7..443d1e1 100644
--- a/src/core/thread/network_data_tlvs.hpp
+++ b/src/core/thread/network_data_tlvs.hpp
@@ -73,10 +73,7 @@
  * @returns A `TlvType` pointer to `aTlv`.
  *
  */
-template <class TlvType> TlvType *As(NetworkDataTlv *aTlv)
-{
-    return static_cast<TlvType *>(aTlv);
-}
+template <class TlvType> TlvType *As(NetworkDataTlv *aTlv) { return static_cast<TlvType *>(aTlv); }
 
 /**
  * This template method casts a `NetworkDataTlv` pointer to a given subclass `TlvType` pointer.
@@ -88,10 +85,7 @@
  * @returns A `TlvType` pointer to `aTlv`.
  *
  */
-template <class TlvType> const TlvType *As(const NetworkDataTlv *aTlv)
-{
-    return static_cast<const TlvType *>(aTlv);
-}
+template <class TlvType> const TlvType *As(const NetworkDataTlv *aTlv) { return static_cast<const TlvType *>(aTlv); }
 
 /**
  * This template method casts a `NetworkDataTlv` reference to a given subclass `TlvType` reference.
@@ -103,10 +97,7 @@
  * @returns A `TlvType` reference to `aTlv`.
  *
  */
-template <class TlvType> TlvType &As(NetworkDataTlv &aTlv)
-{
-    return static_cast<TlvType &>(aTlv);
-}
+template <class TlvType> TlvType &As(NetworkDataTlv &aTlv) { return static_cast<TlvType &>(aTlv); }
 
 /**
  * This template method casts a `NetworkDataTlv` reference to a given subclass `TlvType` reference.
@@ -118,10 +109,7 @@
  * @returns A `TlvType` reference to `aTlv`.
  *
  */
-template <class TlvType> const TlvType &As(const NetworkDataTlv &aTlv)
-{
-    return static_cast<const TlvType &>(aTlv);
-}
+template <class TlvType> const TlvType &As(const NetworkDataTlv &aTlv) { return static_cast<const TlvType &>(aTlv); }
 
 /**
  * This class implements Thread Network Data TLV generation and parsing.
@@ -1561,7 +1549,7 @@
 
 private:
     const uint8_t *GetServerData(void) const { return reinterpret_cast<const uint8_t *>(this) + sizeof(*this); }
-    uint8_t *      GetServerData(void) { return AsNonConst(AsConst(this)->GetServerData()); }
+    uint8_t       *GetServerData(void) { return AsNonConst(AsConst(this)->GetServerData()); }
 
     uint16_t mServer16;
 } OT_TOOL_PACKED_END;
diff --git a/src/core/thread/network_data_types.cpp b/src/core/thread/network_data_types.cpp
index 628f780..24440cf 100644
--- a/src/core/thread/network_data_types.cpp
+++ b/src/core/thread/network_data_types.cpp
@@ -131,8 +131,8 @@
 
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
 
-void OnMeshPrefixConfig::SetFrom(const PrefixTlv &        aPrefixTlv,
-                                 const BorderRouterTlv &  aBorderRouterTlv,
+void OnMeshPrefixConfig::SetFrom(const PrefixTlv         &aPrefixTlv,
+                                 const BorderRouterTlv   &aBorderRouterTlv,
                                  const BorderRouterEntry &aBorderRouterEntry)
 {
     Clear();
@@ -191,9 +191,9 @@
 
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
 
-void ExternalRouteConfig::SetFrom(Instance &           aInstance,
-                                  const PrefixTlv &    aPrefixTlv,
-                                  const HasRouteTlv &  aHasRouteTlv,
+void ExternalRouteConfig::SetFrom(Instance            &aInstance,
+                                  const PrefixTlv     &aPrefixTlv,
+                                  const HasRouteTlv   &aHasRouteTlv,
                                   const HasRouteEntry &aHasRouteEntry)
 {
     Clear();
@@ -249,5 +249,13 @@
     GetServerConfig().SetFrom(aServerTlv);
 }
 
+void LowpanContextInfo::SetFrom(const PrefixTlv &aPrefixTlv, const ContextTlv &aContextTlv)
+{
+    mContextId    = aContextTlv.GetContextId();
+    mCompressFlag = aContextTlv.IsCompress();
+    aPrefixTlv.CopyPrefixTo(GetPrefix());
+    GetPrefix().SetLength(aContextTlv.GetContextLength());
+}
+
 } // namespace NetworkData
 } // namespace ot
diff --git a/src/core/thread/network_data_types.hpp b/src/core/thread/network_data_types.hpp
index 8215343..642b398 100644
--- a/src/core/thread/network_data_types.hpp
+++ b/src/core/thread/network_data_types.hpp
@@ -43,6 +43,7 @@
 #include "common/data.hpp"
 #include "common/debug.hpp"
 #include "common/equatable.hpp"
+#include "common/preference.hpp"
 #include "net/ip6_address.hpp"
 
 namespace ot {
@@ -67,6 +68,7 @@
 class HasRouteEntry;
 class ServiceTlv;
 class ServerTlv;
+class ContextTlv;
 
 /**
  * This enumeration represents the Network Data type.
@@ -89,6 +91,10 @@
     kRoutePreferenceHigh   = OT_ROUTE_PREFERENCE_HIGH, ///< High route preference.
 };
 
+static_assert(kRoutePreferenceHigh == Preference::kHigh, "kRoutePreferenceHigh is not valid");
+static_assert(kRoutePreferenceMedium == Preference::kMedium, "kRoutePreferenceMedium is not valid");
+static_assert(kRoutePreferenceLow == Preference::kLow, "kRoutePreferenceLow is not valid");
+
 /**
  * This enumeration represents the border router RLOC role filter used when searching for border routers in the Network
  * Data.
@@ -111,10 +117,7 @@
  * @retval FALSE  if @p aPref is not valid
  *
  */
-inline bool IsRoutePreferenceValid(int8_t aPref)
-{
-    return (aPref == kRoutePreferenceLow) || (aPref == kRoutePreferenceMedium) || (aPref == kRoutePreferenceHigh);
-}
+inline bool IsRoutePreferenceValid(int8_t aPref) { return Preference::IsValid(aPref); }
 
 /**
  * This function coverts a route preference to a 2-bit unsigned value.
@@ -126,16 +129,7 @@
  * @returns The 2-bit unsigned value representing @p aPref.
  *
  */
-inline uint8_t RoutePreferenceToValue(int8_t aPref)
-{
-    constexpr uint8_t kHigh   = 1; // 01
-    constexpr uint8_t kMedium = 0; // 00
-    constexpr uint8_t kLow    = 3; // 11
-
-    OT_ASSERT(IsRoutePreferenceValid(aPref));
-
-    return (aPref == 0) ? kMedium : ((aPref > 0) ? kHigh : kLow);
-}
+inline uint8_t RoutePreferenceToValue(int8_t aPref) { return Preference::To2BitUint(aPref); }
 
 /**
  * This function coverts a 2-bit unsigned value to a route preference.
@@ -148,19 +142,20 @@
  */
 inline RoutePreference RoutePreferenceFromValue(uint8_t aValue)
 {
-    constexpr uint8_t kMask = 3; // First two bits.
-
-    static const RoutePreference kRoutePreferences[] = {
-        /* 0 (00)  -> */ kRoutePreferenceMedium,
-        /* 1 (01)  -> */ kRoutePreferenceHigh,
-        /* 2 (10)  -> */ kRoutePreferenceMedium, // Per RFC-4191, the reserved value (10) MUST be treated as (00)
-        /* 3 (11)  -> */ kRoutePreferenceLow,
-    };
-
-    return kRoutePreferences[aValue & kMask];
+    return static_cast<RoutePreference>(Preference::From2BitUint(aValue));
 }
 
 /**
+ * This function converts a router preference to a human-readable string.
+ *
+ * @param[in] aPreference  The preference to convert
+ *
+ * @returns The string representation of @p aPreference.
+ *
+ */
+inline const char *RoutePreferenceToString(RoutePreference aPreference) { return Preference::ToString(aPreference); }
+
+/**
  * This class represents an On-mesh Prefix (Border Router) configuration.
  *
  */
@@ -215,8 +210,8 @@
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
     uint16_t ConvertToTlvFlags(void) const;
 #endif
-    void SetFrom(const PrefixTlv &        aPrefixTlv,
-                 const BorderRouterTlv &  aBorderRouterTlv,
+    void SetFrom(const PrefixTlv         &aPrefixTlv,
+                 const BorderRouterTlv   &aBorderRouterTlv,
                  const BorderRouterEntry &aBorderRouterEntry);
     void SetFromTlvFlags(uint16_t aFlags);
 };
@@ -275,14 +270,36 @@
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
     uint8_t ConvertToTlvFlags(void) const;
 #endif
-    void SetFrom(Instance &           aInstance,
-                 const PrefixTlv &    aPrefixTlv,
-                 const HasRouteTlv &  aHasRouteTlv,
+    void SetFrom(Instance            &aInstance,
+                 const PrefixTlv     &aPrefixTlv,
+                 const HasRouteTlv   &aHasRouteTlv,
                  const HasRouteEntry &aHasRouteEntry);
     void SetFromTlvFlags(uint8_t aFlags);
 };
 
 /**
+ * This class represents 6LoWPAN Context ID information associated with a prefix in Network Data.
+ *
+ */
+class LowpanContextInfo : public otLowpanContextInfo, public Clearable<LowpanContextInfo>
+{
+    friend class NetworkData;
+
+public:
+    /**
+     * This method gets the prefix.
+     *
+     * @return The prefix.
+     *
+     */
+    const Ip6::Prefix &GetPrefix(void) const { return AsCoreType(&mPrefix); }
+
+private:
+    Ip6::Prefix &GetPrefix(void) { return AsCoreType(&mPrefix); }
+    void         SetFrom(const PrefixTlv &aPrefixTlv, const ContextTlv &aContextTlv);
+};
+
+/**
  * This class represents a Service Data.
  *
  */
@@ -382,6 +399,7 @@
 
 DefineCoreType(otBorderRouterConfig, NetworkData::OnMeshPrefixConfig);
 DefineCoreType(otExternalRouteConfig, NetworkData::ExternalRouteConfig);
+DefineCoreType(otLowpanContextInfo, NetworkData::LowpanContextInfo);
 DefineCoreType(otServiceConfig, NetworkData::ServiceConfig);
 DefineCoreType(otServerConfig, NetworkData::ServiceConfig::ServerConfig);
 
diff --git a/src/core/thread/network_diagnostic.cpp b/src/core/thread/network_diagnostic.cpp
index 738e904..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"
@@ -50,7 +48,7 @@
 #include "thread/mle_router.hpp"
 #include "thread/thread_netif.hpp"
 #include "thread/thread_tlvs.hpp"
-#include "thread/uri_paths.hpp"
+#include "thread/version.hpp"
 
 namespace ot {
 
@@ -58,135 +56,88 @@
 
 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)
-    , mDiagnosticGetRequest(UriPath::kDiagnosticGetRequest, &NetworkDiagnostic::HandleDiagnosticGetRequest, this)
-    , mDiagnosticGetQuery(UriPath::kDiagnosticGetQuery, &NetworkDiagnostic::HandleDiagnosticGetQuery, this)
-    , mDiagnosticGetAnswer(UriPath::kDiagnosticGetAnswer, &NetworkDiagnostic::HandleDiagnosticGetAnswer, this)
-    , mDiagnosticReset(UriPath::kDiagnosticReset, &NetworkDiagnostic::HandleDiagnosticReset, this)
-    , mReceiveDiagnosticGetCallback(nullptr)
-    , mReceiveDiagnosticGetCallbackContext(nullptr)
 {
-    Get<Tmf::Agent>().AddResource(mDiagnosticGetRequest);
-    Get<Tmf::Agent>().AddResource(mDiagnosticGetQuery);
-    Get<Tmf::Agent>().AddResource(mDiagnosticGetAnswer);
-    Get<Tmf::Agent>().AddResource(mDiagnosticReset);
+    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,
-                                           otReceiveDiagnosticGetCallback aCallback,
-                                           void *                         aCallbackContext)
-{
-    Error                 error;
-    Coap::Message *       message = nullptr;
-    Tmf::MessageInfo      messageInfo(GetInstance());
-    otCoapResponseHandler handler = nullptr;
+#if OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
 
+Error Server::SetVendorName(const char *aVendorName)
+{
+    return SetVendorString(mVendorName, sizeof(mVendorName), aVendorName);
+}
+
+Error Server::SetVendorModel(const char *aVendorModel)
+{
+    return SetVendorString(mVendorModel, sizeof(mVendorModel), aVendorModel);
+}
+
+Error Server::SetVendorSwVersion(const char *aVendorSwVersion)
+{
+    return SetVendorString(mVendorSwVersion, sizeof(mVendorSwVersion), aVendorSwVersion);
+}
+
+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;
+}
+
+#endif // OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+
+void Server::PrepareMessageInfoForDest(const Ip6::Address &aDestination, Tmf::MessageInfo &aMessageInfo) const
+{
     if (aDestination.IsMulticast())
     {
-        message = Get<Tmf::Agent>().NewNonConfirmablePostMessage(UriPath::kDiagnosticGetQuery);
-        messageInfo.SetMulticastLoop(true);
-    }
-    else
-    {
-        handler = &NetworkDiagnostic::HandleDiagnosticGetResponse;
-        message = Get<Tmf::Agent>().NewConfirmablePostMessage(UriPath::kDiagnosticGetRequest);
-    }
-
-    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
-
-    if (aCount > 0)
-    {
-        SuccessOrExit(error = Tlv::Append<TypeListTlv>(*message, aTlvTypes, aCount));
+        aMessageInfo.SetMulticastLoop(true);
     }
 
     if (aDestination.IsLinkLocal() || aDestination.IsLinkLocalMulticast())
     {
-        messageInfo.SetSockAddr(Get<Mle::MleRouter>().GetLinkLocalAddress());
+        aMessageInfo.SetSockAddr(Get<Mle::MleRouter>().GetLinkLocalAddress());
     }
     else
     {
-        messageInfo.SetSockAddrToRloc();
+        aMessageInfo.SetSockAddrToRloc();
     }
 
-    messageInfo.SetPeerAddr(aDestination);
-
-    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, handler, this));
-
-    mReceiveDiagnosticGetCallback        = aCallback;
-    mReceiveDiagnosticGetCallbackContext = aCallbackContext;
-
-    LogInfo("Sent diagnostic get");
-
-exit:
-    FreeMessageOnError(message, error);
-    return error;
+    aMessageInfo.SetPeerAddr(aDestination);
 }
 
-void NetworkDiagnostic::HandleDiagnosticGetResponse(void *               aContext,
-                                                    otMessage *          aMessage,
-                                                    const otMessageInfo *aMessageInfo,
-                                                    Error                aResult)
+Error Server::AppendIp6AddressList(Message &aMessage)
 {
-    static_cast<NetworkDiagnostic *>(aContext)->HandleDiagnosticGetResponse(AsCoapMessagePtr(aMessage),
-                                                                            AsCoreTypePtr(aMessageInfo), aResult);
-}
-
-void NetworkDiagnostic::HandleDiagnosticGetResponse(Coap::Message *         aMessage,
-                                                    const Ip6::MessageInfo *aMessageInfo,
-                                                    Error                   aResult)
-{
-    SuccessOrExit(aResult);
-    VerifyOrExit(aMessage->GetCode() == Coap::kCodeChanged, aResult = kErrorFailed);
-
-exit:
-    if (mReceiveDiagnosticGetCallback)
-    {
-        mReceiveDiagnosticGetCallback(aResult, aMessage, aMessageInfo, mReceiveDiagnosticGetCallbackContext);
-    }
-    else
-    {
-        LogDebg("Received diagnostic get response, error = %s", ErrorToString(aResult));
-    }
-    return;
-}
-
-void NetworkDiagnostic::HandleDiagnosticGetAnswer(void *               aContext,
-                                                  otMessage *          aMessage,
-                                                  const otMessageInfo *aMessageInfo)
-{
-    static_cast<NetworkDiagnostic *>(aContext)->HandleDiagnosticGetAnswer(AsCoapMessage(aMessage),
-                                                                          AsCoreType(aMessageInfo));
-}
-
-void NetworkDiagnostic::HandleDiagnosticGetAnswer(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
-{
-    VerifyOrExit(aMessage.IsConfirmablePostRequest());
-
-    LogInfo("Diagnostic get answer received");
-
-    if (mReceiveDiagnosticGetCallback)
-    {
-        mReceiveDiagnosticGetCallback(kErrorNone, &aMessage, &aMessageInfo, mReceiveDiagnosticGetCallbackContext);
-    }
-
-    SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
-
-    LogInfo("Sent diagnostic answer acknowledgment");
-
-exit:
-    return;
-}
-
-Error NetworkDiagnostic::AppendIp6AddressList(Message &aMessage)
-{
-    Error             error = kErrorNone;
-    Ip6AddressListTlv tlv;
-    uint8_t           count = 0;
-
-    tlv.Init();
+    Error    error = kErrorNone;
+    uint16_t count = 0;
 
     for (const Ip6::Netif::UnicastAddress &addr : Get<ThreadNetif>().GetUnicastAddresses())
     {
@@ -194,8 +145,22 @@
         count++;
     }
 
-    tlv.SetLength(count * sizeof(Ip6::Address));
-    SuccessOrExit(error = aMessage.Append(tlv));
+    if (count * Ip6::Address::kSize <= Tlv::kBaseTlvMaxLength)
+    {
+        Tlv tlv;
+
+        tlv.SetType(Tlv::kIp6AddressList);
+        tlv.SetLength(static_cast<uint8_t>(count * Ip6::Address::kSize));
+        SuccessOrExit(error = aMessage.Append(tlv));
+    }
+    else
+    {
+        ExtendedTlv extTlv;
+
+        extTlv.SetType(Tlv::kIp6AddressList);
+        extTlv.SetLength(count * Ip6::Address::kSize);
+        SuccessOrExit(error = aMessage.Append(extTlv));
+    }
 
     for (const Ip6::Netif::UnicastAddress &addr : Get<ThreadNetif>().GetUnicastAddresses())
     {
@@ -203,400 +168,305 @@
     }
 
 exit:
-
     return error;
 }
 
 #if OPENTHREAD_FTD
-Error NetworkDiagnostic::AppendChildTable(Message &aMessage)
+Error Server::AppendChildTable(Message &aMessage)
 {
-    Error           error   = kErrorNone;
-    uint16_t        count   = 0;
-    uint8_t         timeout = 0;
-    ChildTableTlv   tlv;
-    ChildTableEntry entry;
+    Error    error = kErrorNone;
+    uint16_t count;
 
-    tlv.Init();
+    VerifyOrExit(Get<Mle::MleRouter>().IsRouterOrLeader());
 
-    count = Get<ChildTable>().GetNumChildren(Child::kInStateValid);
+    count = Min(Get<ChildTable>().GetNumChildren(Child::kInStateValid), kMaxChildEntries);
 
-    // The length of the Child Table TLV may exceed the outgoing link's MTU (1280B).
-    // As a workaround we limit the number of entries in the Child Table TLV,
-    // also to avoid using extended TLV format. The issue is processed by the
-    // Thread Group (SPEC-894).
-    if (count > (Tlv::kBaseTlvMaxLength / sizeof(ChildTableEntry)))
+    if (count * sizeof(ChildTableEntry) <= Tlv::kBaseTlvMaxLength)
     {
-        count = Tlv::kBaseTlvMaxLength / sizeof(ChildTableEntry);
+        Tlv tlv;
+
+        tlv.SetType(Tlv::kChildTable);
+        tlv.SetLength(static_cast<uint8_t>(count * sizeof(ChildTableEntry)));
+        SuccessOrExit(error = aMessage.Append(tlv));
     }
+    else
+    {
+        ExtendedTlv extTlv;
 
-    tlv.SetLength(static_cast<uint8_t>(count * sizeof(ChildTableEntry)));
-
-    SuccessOrExit(error = aMessage.Append(tlv));
+        extTlv.SetType(Tlv::kChildTable);
+        extTlv.SetLength(count * sizeof(ChildTableEntry));
+        SuccessOrExit(error = aMessage.Append(extTlv));
+    }
 
     for (Child &child : Get<ChildTable>().Iterate(Child::kInStateValid))
     {
-        VerifyOrExit(count--);
+        uint8_t         timeout = 0;
+        ChildTableEntry entry;
 
-        timeout = 0;
+        VerifyOrExit(count--);
 
         while (static_cast<uint32_t>(1 << timeout) < child.GetTimeout())
         {
             timeout++;
         }
 
-        entry.SetReserved(0);
+        entry.Clear();
         entry.SetTimeout(timeout + 4);
-
-        entry.SetChildId(Mle::Mle::ChildIdFromRloc16(child.GetRloc16()));
+        entry.SetLinkQuality(child.GetLinkQualityIn());
+        entry.SetChildId(Mle::ChildIdFromRloc16(child.GetRloc16()));
         entry.SetMode(child.GetDeviceMode());
 
         SuccessOrExit(error = aMessage.Append(entry));
     }
 
 exit:
-
     return error;
 }
 #endif // OPENTHREAD_FTD
 
-void NetworkDiagnostic::FillMacCountersTlv(MacCountersTlv &aMacCountersTlv)
+Error Server::AppendMacCounters(Message &aMessage)
 {
-    const otMacCounters &macCounters = Get<Mac::Mac>().GetCounters();
+    MacCountersTlv       tlv;
+    const otMacCounters &counters = Get<Mac::Mac>().GetCounters();
 
-    aMacCountersTlv.SetIfInUnknownProtos(macCounters.mRxOther);
-    aMacCountersTlv.SetIfInErrors(macCounters.mRxErrNoFrame + macCounters.mRxErrUnknownNeighbor +
-                                  macCounters.mRxErrInvalidSrcAddr + macCounters.mRxErrSec + macCounters.mRxErrFcs +
-                                  macCounters.mRxErrOther);
-    aMacCountersTlv.SetIfOutErrors(macCounters.mTxErrCca);
-    aMacCountersTlv.SetIfInUcastPkts(macCounters.mRxUnicast);
-    aMacCountersTlv.SetIfInBroadcastPkts(macCounters.mRxBroadcast);
-    aMacCountersTlv.SetIfInDiscards(macCounters.mRxAddressFiltered + macCounters.mRxDestAddrFiltered +
-                                    macCounters.mRxDuplicated);
-    aMacCountersTlv.SetIfOutUcastPkts(macCounters.mTxUnicast);
-    aMacCountersTlv.SetIfOutBroadcastPkts(macCounters.mTxBroadcast);
-    aMacCountersTlv.SetIfOutDiscards(macCounters.mTxErrBusyChannel);
+    memset(&tlv, 0, sizeof(tlv));
+
+    tlv.Init();
+    tlv.SetIfInUnknownProtos(counters.mRxOther);
+    tlv.SetIfInErrors(counters.mRxErrNoFrame + counters.mRxErrUnknownNeighbor + counters.mRxErrInvalidSrcAddr +
+                      counters.mRxErrSec + counters.mRxErrFcs + counters.mRxErrOther);
+    tlv.SetIfOutErrors(counters.mTxErrCca);
+    tlv.SetIfInUcastPkts(counters.mRxUnicast);
+    tlv.SetIfInBroadcastPkts(counters.mRxBroadcast);
+    tlv.SetIfInDiscards(counters.mRxAddressFiltered + counters.mRxDestAddrFiltered + counters.mRxDuplicated);
+    tlv.SetIfOutUcastPkts(counters.mTxUnicast);
+    tlv.SetIfOutBroadcastPkts(counters.mTxBroadcast);
+    tlv.SetIfOutDiscards(counters.mTxErrBusyChannel);
+
+    return tlv.AppendTo(aMessage);
 }
 
-Error NetworkDiagnostic::FillRequestedTlvs(const Message &       aRequest,
-                                           Message &             aResponse,
-                                           NetworkDiagnosticTlv &aNetworkDiagnosticTlv)
+Error Server::AppendRequestedTlvs(const Message &aRequest, Message &aResponse)
 {
-    Error    error  = kErrorNone;
-    uint16_t offset = 0;
-    uint8_t  type;
+    Error    error;
+    uint16_t offset;
+    uint16_t length;
+    uint16_t endOffset;
 
-    offset = aRequest.GetOffset() + sizeof(NetworkDiagnosticTlv);
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aRequest, Tlv::kTypeList, offset, length));
+    endOffset = offset + length;
 
-    for (uint32_t i = 0; i < aNetworkDiagnosticTlv.GetLength(); i++)
+    for (; offset < endOffset; offset++)
     {
-        SuccessOrExit(error = aRequest.Read(offset, type));
+        uint8_t tlvType;
 
-        LogInfo("Type %d", type);
-
-        switch (type)
-        {
-        case NetworkDiagnosticTlv::kExtMacAddress:
-            SuccessOrExit(error = Tlv::Append<ExtMacAddressTlv>(aResponse, Get<Mac::Mac>().GetExtAddress()));
-            break;
-
-        case NetworkDiagnosticTlv::kAddress16:
-            SuccessOrExit(error = Tlv::Append<Address16Tlv>(aResponse, Get<Mle::MleRouter>().GetRloc16()));
-            break;
-
-        case NetworkDiagnosticTlv::kMode:
-            SuccessOrExit(error = Tlv::Append<ModeTlv>(aResponse, Get<Mle::MleRouter>().GetDeviceMode().Get()));
-            break;
-
-        case NetworkDiagnosticTlv::kTimeout:
-            if (!Get<Mle::MleRouter>().IsRxOnWhenIdle())
-            {
-                SuccessOrExit(error = Tlv::Append<TimeoutTlv>(aResponse, Get<Mle::MleRouter>().GetTimeout()));
-            }
-
-            break;
-
-#if OPENTHREAD_FTD
-        case NetworkDiagnosticTlv::kConnectivity:
-        {
-            ConnectivityTlv tlv;
-            tlv.Init();
-            Get<Mle::MleRouter>().FillConnectivityTlv(reinterpret_cast<Mle::ConnectivityTlv &>(tlv));
-            SuccessOrExit(error = tlv.AppendTo(aResponse));
-            break;
-        }
-
-        case NetworkDiagnosticTlv::kRoute:
-        {
-            RouteTlv tlv;
-            tlv.Init();
-            Get<Mle::MleRouter>().FillRouteTlv(reinterpret_cast<Mle::RouteTlv &>(tlv));
-            SuccessOrExit(error = tlv.AppendTo(aResponse));
-            break;
-        }
-#endif
-
-        case NetworkDiagnosticTlv::kLeaderData:
-        {
-            LeaderDataTlv          tlv;
-            const Mle::LeaderData &leaderData = Get<Mle::MleRouter>().GetLeaderData();
-
-            tlv.Init();
-            tlv.SetPartitionId(leaderData.GetPartitionId());
-            tlv.SetWeighting(leaderData.GetWeighting());
-            tlv.SetDataVersion(leaderData.GetDataVersion(NetworkData::kFullSet));
-            tlv.SetStableDataVersion(leaderData.GetDataVersion(NetworkData::kStableSubset));
-            tlv.SetLeaderRouterId(leaderData.GetLeaderRouterId());
-
-            SuccessOrExit(error = tlv.AppendTo(aResponse));
-            break;
-        }
-
-        case NetworkDiagnosticTlv::kNetworkData:
-        {
-            NetworkData::NetworkData &netData = Get<NetworkData::Leader>();
-
-            SuccessOrExit(error = Tlv::Append<NetworkDataTlv>(aResponse, netData.GetBytes(), netData.GetLength()));
-            break;
-        }
-
-        case NetworkDiagnosticTlv::kIp6AddressList:
-        {
-            SuccessOrExit(error = AppendIp6AddressList(aResponse));
-            break;
-        }
-
-        case NetworkDiagnosticTlv::kMacCounters:
-        {
-            MacCountersTlv tlv;
-            memset(&tlv, 0, sizeof(tlv));
-            tlv.Init();
-            FillMacCountersTlv(tlv);
-            SuccessOrExit(error = tlv.AppendTo(aResponse));
-            break;
-        }
-
-        case NetworkDiagnosticTlv::kBatteryLevel:
-        {
-            // Thread 1.1.1 Specification Section 10.11.4.2:
-            // Omitted if the battery level is not measured, is unknown or the device does not
-            // operate on battery power.
-            break;
-        }
-
-        case NetworkDiagnosticTlv::kSupplyVoltage:
-        {
-            // Thread 1.1.1 Specification Section 10.11.4.3:
-            // Omitted if the supply voltage is not measured, is unknown.
-            break;
-        }
-
-#if OPENTHREAD_FTD
-        case NetworkDiagnosticTlv::kChildTable:
-        {
-            // Thread 1.1.1 Specification Section 10.11.2.2:
-            // If a Thread device is unable to supply a specific Diagnostic TLV, that TLV is omitted.
-            // Here only Leader or Router may have children.
-            if (Get<Mle::MleRouter>().IsRouterOrLeader())
-            {
-                SuccessOrExit(error = AppendChildTable(aResponse));
-            }
-            break;
-        }
-#endif
-
-        case NetworkDiagnosticTlv::kChannelPages:
-        {
-            uint8_t         length   = 0;
-            uint32_t        pageMask = Radio::kSupportedChannelPages;
-            ChannelPagesTlv tlv;
-
-            tlv.Init();
-            for (uint8_t page = 0; page < sizeof(pageMask) * 8; page++)
-            {
-                if (pageMask & (1 << page))
-                {
-                    tlv.GetChannelPages()[length++] = page;
-                }
-            }
-
-            tlv.SetLength(length);
-            SuccessOrExit(error = tlv.AppendTo(aResponse));
-            break;
-        }
-
-#if OPENTHREAD_FTD
-        case NetworkDiagnosticTlv::kMaxChildTimeout:
-        {
-            uint32_t maxTimeout;
-
-            if (Get<Mle::MleRouter>().GetMaxChildTimeout(maxTimeout) == kErrorNone)
-            {
-                SuccessOrExit(error = Tlv::Append<MaxChildTimeoutTlv>(aResponse, maxTimeout));
-            }
-
-            break;
-        }
-#endif
-
-        default:
-            // Skip unrecognized TLV type.
-            break;
-        }
-
-        offset += sizeof(type);
+        SuccessOrExit(error = aRequest.Read(offset, tlvType));
+        SuccessOrExit(error = AppendDiagTlv(tlvType, aResponse));
     }
 
 exit:
     return error;
 }
 
-void NetworkDiagnostic::HandleDiagnosticGetQuery(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
+Error Server::AppendDiagTlv(uint8_t aTlvType, Message &aMessage)
 {
-    static_cast<NetworkDiagnostic *>(aContext)->HandleDiagnosticGetQuery(AsCoapMessage(aMessage),
-                                                                         AsCoreType(aMessageInfo));
+    Error error = kErrorNone;
+
+    switch (aTlvType)
+    {
+    case Tlv::kExtMacAddress:
+        error = Tlv::Append<ExtMacAddressTlv>(aMessage, Get<Mac::Mac>().GetExtAddress());
+        break;
+
+    case Tlv::kAddress16:
+        error = Tlv::Append<Address16Tlv>(aMessage, Get<Mle::MleRouter>().GetRloc16());
+        break;
+
+    case Tlv::kMode:
+        error = Tlv::Append<ModeTlv>(aMessage, Get<Mle::MleRouter>().GetDeviceMode().Get());
+        break;
+
+    case Tlv::kVersion:
+        error = Tlv::Append<VersionTlv>(aMessage, kThreadVersion);
+        break;
+
+    case Tlv::kTimeout:
+        VerifyOrExit(!Get<Mle::MleRouter>().IsRxOnWhenIdle());
+        error = Tlv::Append<TimeoutTlv>(aMessage, Get<Mle::MleRouter>().GetTimeout());
+        break;
+
+    case Tlv::kLeaderData:
+    {
+        LeaderDataTlv tlv;
+
+        tlv.Init();
+        tlv.Set(Get<Mle::MleRouter>().GetLeaderData());
+        error = tlv.AppendTo(aMessage);
+        break;
+    }
+
+    case Tlv::kNetworkData:
+        error = Tlv::Append<NetworkDataTlv>(aMessage, Get<NetworkData::Leader>().GetBytes(),
+                                            Get<NetworkData::Leader>().GetLength());
+        break;
+
+    case Tlv::kIp6AddressList:
+        error = AppendIp6AddressList(aMessage);
+        break;
+
+    case Tlv::kMacCounters:
+        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;
+        uint8_t         length = 0;
+
+        tlv.Init();
+
+        for (uint8_t page = 0; page < sizeof(Radio::kSupportedChannelPages) * CHAR_BIT; page++)
+        {
+            if (Radio::kSupportedChannelPages & (1 << page))
+            {
+                tlv.GetChannelPages()[length++] = page;
+            }
+        }
+
+        tlv.SetLength(length);
+        error = tlv.AppendTo(aMessage);
+
+        break;
+    }
+
+#if OPENTHREAD_FTD
+
+    case Tlv::kConnectivity:
+    {
+        ConnectivityTlv tlv;
+
+        tlv.Init();
+        Get<Mle::MleRouter>().FillConnectivityTlv(tlv);
+        error = tlv.AppendTo(aMessage);
+        break;
+    }
+
+    case Tlv::kRoute:
+    {
+        RouteTlv tlv;
+
+        tlv.Init();
+        Get<RouterTable>().FillRouteTlv(tlv);
+        SuccessOrExit(error = tlv.AppendTo(aMessage));
+        break;
+    }
+
+    case Tlv::kChildTable:
+        error = AppendChildTable(aMessage);
+        break;
+
+    case Tlv::kMaxChildTimeout:
+    {
+        uint32_t maxTimeout;
+
+        SuccessOrExit(Get<Mle::MleRouter>().GetMaxChildTimeout(maxTimeout));
+        error = Tlv::Append<MaxChildTimeoutTlv>(aMessage, maxTimeout);
+        break;
+    }
+
+#endif // OPENTHREAD_FTD
+
+    default:
+        break;
+    }
+
+exit:
+    return error;
 }
 
-void NetworkDiagnostic::HandleDiagnosticGetQuery(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <>
+void Server::HandleTmf<kUriDiagnosticGetQuery>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    Error                error   = kErrorNone;
-    Coap::Message *      message = nullptr;
-    NetworkDiagnosticTlv networkDiagnosticTlv;
-    Tmf::MessageInfo     messageInfo(GetInstance());
+    Error            error    = kErrorNone;
+    Coap::Message   *response = nullptr;
+    Tmf::MessageInfo responseInfo(GetInstance());
 
     VerifyOrExit(aMessage.IsPostRequest(), error = kErrorDrop);
 
-    LogInfo("Received diagnostic get query");
+    LogInfo("Received %s from %s", UriToString<kUriDiagnosticGetQuery>(),
+            aMessageInfo.GetPeerAddr().ToString().AsCString());
 
-    SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), networkDiagnosticTlv));
-
-    VerifyOrExit(networkDiagnosticTlv.GetType() == NetworkDiagnosticTlv::kTypeList, error = kErrorParse);
-
-    // DIAG_GET.qry may be sent as a confirmable message.
+    // DIAG_GET.qry may be sent as a confirmable request.
     if (aMessage.IsConfirmable())
     {
-        if (Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo) == kErrorNone)
-        {
-            LogInfo("Sent diagnostic get query acknowledgment");
-        }
+        IgnoreError(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
     }
 
-    message = Get<Tmf::Agent>().NewConfirmablePostMessage(UriPath::kDiagnosticGetAnswer);
-    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
+    response = Get<Tmf::Agent>().NewConfirmablePostMessage(kUriDiagnosticGetAnswer);
+    VerifyOrExit(response != nullptr, error = kErrorNoBufs);
 
-    if (aMessageInfo.GetPeerAddr().IsLinkLocal())
-    {
-        messageInfo.SetSockAddr(Get<Mle::MleRouter>().GetLinkLocalAddress());
-    }
-    else
-    {
-        messageInfo.SetSockAddrToRloc();
-    }
+    SuccessOrExit(error = AppendRequestedTlvs(aMessage, *response));
 
-    messageInfo.SetPeerAddr(aMessageInfo.GetPeerAddr());
-
-    SuccessOrExit(error = FillRequestedTlvs(aMessage, *message, networkDiagnosticTlv));
-
-    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, nullptr, this));
-
-    LogInfo("Sent diagnostic get answer");
+    PrepareMessageInfoForDest(aMessageInfo.GetPeerAddr(), responseInfo);
+    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*response, responseInfo));
 
 exit:
-    FreeMessageOnError(message, error);
+    FreeMessageOnError(response, error);
 }
 
-void NetworkDiagnostic::HandleDiagnosticGetRequest(void *               aContext,
-                                                   otMessage *          aMessage,
-                                                   const otMessageInfo *aMessageInfo)
+template <>
+void Server::HandleTmf<kUriDiagnosticGetRequest>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    static_cast<NetworkDiagnostic *>(aContext)->HandleDiagnosticGetRequest(AsCoapMessage(aMessage),
-                                                                           AsCoreType(aMessageInfo));
-}
-
-void NetworkDiagnostic::HandleDiagnosticGetRequest(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
-{
-    Error                error   = kErrorNone;
-    Coap::Message *      message = nullptr;
-    NetworkDiagnosticTlv networkDiagnosticTlv;
-    Ip6::MessageInfo     messageInfo(aMessageInfo);
+    Error          error    = kErrorNone;
+    Coap::Message *response = nullptr;
 
     VerifyOrExit(aMessage.IsConfirmablePostRequest(), error = kErrorDrop);
 
-    LogInfo("Received diagnostic get request");
+    LogInfo("Received %s from %s", UriToString<kUriDiagnosticGetRequest>(),
+            aMessageInfo.GetPeerAddr().ToString().AsCString());
 
-    SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), networkDiagnosticTlv));
+    response = Get<Tmf::Agent>().NewResponseMessage(aMessage);
+    VerifyOrExit(response != nullptr, error = kErrorNoBufs);
 
-    VerifyOrExit(networkDiagnosticTlv.GetType() == NetworkDiagnosticTlv::kTypeList, error = kErrorParse);
-
-    message = Get<Tmf::Agent>().NewResponseMessage(aMessage);
-    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
-
-    SuccessOrExit(error = FillRequestedTlvs(aMessage, *message, networkDiagnosticTlv));
-
-    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
-
-    LogInfo("Sent diagnostic get response");
+    SuccessOrExit(error = AppendRequestedTlvs(aMessage, *response));
+    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*response, aMessageInfo));
 
 exit:
-    FreeMessageOnError(message, error);
+    FreeMessageOnError(response, error);
 }
 
-Error NetworkDiagnostic::SendDiagnosticReset(const Ip6::Address &aDestination,
-                                             const uint8_t       aTlvTypes[],
-                                             uint8_t             aCount)
+template <> void Server::HandleTmf<kUriDiagnosticReset>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    Error            error;
-    Coap::Message *  message = nullptr;
-    Tmf::MessageInfo messageInfo(GetInstance());
-
-    message = Get<Tmf::Agent>().NewConfirmablePostMessage(UriPath::kDiagnosticReset);
-    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
-
-    if (aCount > 0)
-    {
-        SuccessOrExit(error = Tlv::Append<TypeListTlv>(*message, aTlvTypes, aCount));
-    }
-
-    if (aDestination.IsLinkLocal() || aDestination.IsLinkLocalMulticast())
-    {
-        messageInfo.SetSockAddr(Get<Mle::MleRouter>().GetLinkLocalAddress());
-    }
-    else
-    {
-        messageInfo.SetSockAddrToRloc();
-    }
-
-    messageInfo.SetPeerAddr(aDestination);
-
-    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
-
-    LogInfo("Sent network diagnostic reset");
-
-exit:
-    FreeMessageOnError(message, error);
-    return error;
-}
-
-void NetworkDiagnostic::HandleDiagnosticReset(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
-{
-    static_cast<NetworkDiagnostic *>(aContext)->HandleDiagnosticReset(AsCoapMessage(aMessage),
-                                                                      AsCoreType(aMessageInfo));
-}
-
-void NetworkDiagnostic::HandleDiagnosticReset(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
-{
-    uint16_t             offset = 0;
-    uint8_t              type;
-    NetworkDiagnosticTlv tlv;
-
-    LogInfo("Received diagnostic reset request");
+    uint16_t offset = 0;
+    uint8_t  type;
+    Tlv      tlv;
 
     VerifyOrExit(aMessage.IsConfirmablePostRequest());
 
+    LogInfo("Received %s from %s", UriToString<kUriDiagnosticReset>(),
+            aMessageInfo.GetPeerAddr().ToString().AsCString());
+
     SuccessOrExit(aMessage.Read(aMessage.GetOffset(), tlv));
 
-    VerifyOrExit(tlv.GetType() == NetworkDiagnosticTlv::kTypeList);
+    VerifyOrExit(tlv.GetType() == Tlv::kTypeList);
 
-    offset = aMessage.GetOffset() + sizeof(NetworkDiagnosticTlv);
+    offset = aMessage.GetOffset() + sizeof(Tlv);
 
     for (uint8_t i = 0; i < tlv.GetLength(); i++)
     {
@@ -604,44 +474,134 @@
 
         switch (type)
         {
-        case NetworkDiagnosticTlv::kMacCounters:
+        case Tlv::kMacCounters:
             Get<Mac::Mac>().ResetCounters();
-            LogInfo("Received diagnostic reset type kMacCounters(9)");
             break;
 
         default:
-            LogInfo("Received diagnostic reset other type %d not resetable", type);
             break;
         }
     }
 
-    SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
-
-    LogInfo("Sent diagnostic reset acknowledgment");
+    IgnoreError(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
 
 exit:
     return;
 }
 
-static inline void ParseMode(const Mle::DeviceMode &aMode, otLinkModeConfig &aLinkModeConfig)
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+
+//---------------------------------------------------------------------------------------------------------------------
+// Client
+
+Client::Client(Instance &aInstance)
+    : InstanceLocator(aInstance)
 {
-    aLinkModeConfig.mRxOnWhenIdle = aMode.IsRxOnWhenIdle();
-    aLinkModeConfig.mDeviceType   = aMode.IsFullThreadDevice();
-    aLinkModeConfig.mNetworkData  = (aMode.GetNetworkDataType() == NetworkData::kFullSet);
 }
 
-static inline void ParseConnectivity(const ConnectivityTlv &    aConnectivityTlv,
-                                     otNetworkDiagConnectivity &aNetworkDiagConnectivity)
+Error Client::SendDiagnosticGet(const Ip6::Address &aDestination,
+                                const uint8_t       aTlvTypes[],
+                                uint8_t             aCount,
+                                GetCallback         aCallback,
+                                void               *aContext)
 {
-    aNetworkDiagConnectivity.mParentPriority   = aConnectivityTlv.GetParentPriority();
-    aNetworkDiagConnectivity.mLinkQuality3     = aConnectivityTlv.GetLinkQuality3();
-    aNetworkDiagConnectivity.mLinkQuality2     = aConnectivityTlv.GetLinkQuality2();
-    aNetworkDiagConnectivity.mLinkQuality1     = aConnectivityTlv.GetLinkQuality1();
-    aNetworkDiagConnectivity.mLeaderCost       = aConnectivityTlv.GetLeaderCost();
-    aNetworkDiagConnectivity.mIdSequence       = aConnectivityTlv.GetIdSequence();
-    aNetworkDiagConnectivity.mActiveRouters    = aConnectivityTlv.GetActiveRouters();
-    aNetworkDiagConnectivity.mSedBufferSize    = aConnectivityTlv.GetSedBufferSize();
-    aNetworkDiagConnectivity.mSedDatagramCount = aConnectivityTlv.GetSedDatagramCount();
+    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)
@@ -664,15 +624,6 @@
     aNetworkDiagRoute.mIdSequence = aRouteTlv.GetRouterIdSequence();
 }
 
-static inline void ParseLeaderData(const LeaderDataTlv &aLeaderDataTlv, otLeaderData &aLeaderData)
-{
-    aLeaderData.mPartitionId       = aLeaderDataTlv.GetPartitionId();
-    aLeaderData.mWeighting         = aLeaderDataTlv.GetWeighting();
-    aLeaderData.mDataVersion       = aLeaderDataTlv.GetDataVersion();
-    aLeaderData.mStableDataVersion = aLeaderDataTlv.GetStableDataVersion();
-    aLeaderData.mLeaderRouterId    = aLeaderDataTlv.GetLeaderRouterId();
-}
-
 static inline void ParseMacCounters(const MacCountersTlv &aMacCountersTlv, otNetworkDiagMacCounters &aMacCounters)
 {
     aMacCounters.mIfInUnknownProtos  = aMacCountersTlv.GetIfInUnknownProtos();
@@ -686,184 +637,266 @@
     aMacCounters.mIfOutDiscards      = aMacCountersTlv.GetIfOutDiscards();
 }
 
-static inline void ParseChildEntry(const ChildTableEntry &aChildTableTlvEntry, otNetworkDiagChildEntry &aChildEntry)
+Error Client::GetNextDiagTlv(const Coap::Message &aMessage, Iterator &aIterator, TlvInfo &aTlvInfo)
 {
-    aChildEntry.mTimeout = aChildTableTlvEntry.GetTimeout();
-    aChildEntry.mChildId = aChildTableTlvEntry.GetChildId();
-    ParseMode(aChildTableTlvEntry.GetMode(), aChildEntry.mMode);
-}
+    Error    error;
+    uint16_t offset = (aIterator == 0) ? aMessage.GetOffset() : aIterator;
 
-Error NetworkDiagnostic::GetNextDiagTlv(const Coap::Message &aMessage,
-                                        Iterator &           aIterator,
-                                        otNetworkDiagTlv &   aNetworkDiagTlv)
-{
-    Error                error  = kErrorNone;
-    uint16_t             offset = aMessage.GetOffset() + aIterator;
-    NetworkDiagnosticTlv tlv;
-
-    while (true)
+    while (offset < aMessage.GetLength())
     {
-        uint16_t tlvTotalLength;
+        bool     skipTlv = false;
+        uint16_t valueOffset;
+        uint16_t tlvLength;
+        union
+        {
+            Tlv         tlv;
+            ExtendedTlv extTlv;
+        };
 
-        VerifyOrExit(aMessage.Read(offset, tlv) == kErrorNone, error = kErrorNotFound);
+        SuccessOrExit(error = aMessage.Read(offset, tlv));
+
+        if (tlv.IsExtended())
+        {
+            SuccessOrExit(error = aMessage.Read(offset, extTlv));
+            valueOffset = offset + sizeof(ExtendedTlv);
+            tlvLength   = extTlv.GetLength();
+        }
+        else
+        {
+            valueOffset = offset + sizeof(Tlv);
+            tlvLength   = tlv.GetLength();
+        }
+
+        VerifyOrExit(offset + tlv.GetSize() <= aMessage.GetLength(), error = kErrorParse);
 
         switch (tlv.GetType())
         {
-        case NetworkDiagnosticTlv::kExtMacAddress:
-            SuccessOrExit(
-                error = Tlv::Read<ExtMacAddressTlv>(aMessage, offset, AsCoreType(&aNetworkDiagTlv.mData.mExtAddress)));
+        case Tlv::kExtMacAddress:
+            SuccessOrExit(error =
+                              Tlv::Read<ExtMacAddressTlv>(aMessage, offset, AsCoreType(&aTlvInfo.mData.mExtAddress)));
             break;
 
-        case NetworkDiagnosticTlv::kAddress16:
-            SuccessOrExit(error = Tlv::Read<Address16Tlv>(aMessage, offset, aNetworkDiagTlv.mData.mAddr16));
+        case Tlv::kAddress16:
+            SuccessOrExit(error = Tlv::Read<Address16Tlv>(aMessage, offset, aTlvInfo.mData.mAddr16));
             break;
 
-        case NetworkDiagnosticTlv::kMode:
+        case Tlv::kMode:
         {
             uint8_t mode;
 
             SuccessOrExit(error = Tlv::Read<ModeTlv>(aMessage, offset, mode));
-            ParseMode(Mle::DeviceMode(mode), aNetworkDiagTlv.mData.mMode);
+            Mle::DeviceMode(mode).Get(aTlvInfo.mData.mMode);
             break;
         }
 
-        case NetworkDiagnosticTlv::kTimeout:
-            SuccessOrExit(error = Tlv::Read<TimeoutTlv>(aMessage, offset, aNetworkDiagTlv.mData.mTimeout));
+        case Tlv::kTimeout:
+            SuccessOrExit(error = Tlv::Read<TimeoutTlv>(aMessage, offset, aTlvInfo.mData.mTimeout));
             break;
 
-        case NetworkDiagnosticTlv::kConnectivity:
+        case Tlv::kConnectivity:
         {
-            ConnectivityTlv connectivity;
+            ConnectivityTlv connectivityTlv;
 
-            SuccessOrExit(error = aMessage.Read(offset, connectivity));
-            VerifyOrExit(connectivity.IsValid(), error = kErrorParse);
-
-            ParseConnectivity(connectivity, aNetworkDiagTlv.mData.mConnectivity);
+            VerifyOrExit(!tlv.IsExtended(), error = kErrorParse);
+            SuccessOrExit(error = aMessage.Read(offset, connectivityTlv));
+            VerifyOrExit(connectivityTlv.IsValid(), error = kErrorParse);
+            connectivityTlv.GetConnectivity(aTlvInfo.mData.mConnectivity);
             break;
         }
 
-        case NetworkDiagnosticTlv::kRoute:
+        case Tlv::kRoute:
         {
-            RouteTlv route;
+            RouteTlv routeTlv;
+            uint16_t bytesToRead = static_cast<uint16_t>(Min(tlv.GetSize(), static_cast<uint32_t>(sizeof(routeTlv))));
 
-            tlvTotalLength = sizeof(tlv) + tlv.GetLength();
-            VerifyOrExit(tlvTotalLength <= sizeof(route), error = kErrorParse);
-            SuccessOrExit(error = aMessage.Read(offset, &route, tlvTotalLength));
-            VerifyOrExit(route.IsValid(), error = kErrorParse);
-
-            ParseRoute(route, aNetworkDiagTlv.mData.mRoute);
+            VerifyOrExit(!tlv.IsExtended(), error = kErrorParse);
+            SuccessOrExit(error = aMessage.Read(offset, &routeTlv, bytesToRead));
+            VerifyOrExit(routeTlv.IsValid(), error = kErrorParse);
+            ParseRoute(routeTlv, aTlvInfo.mData.mRoute);
             break;
         }
 
-        case NetworkDiagnosticTlv::kLeaderData:
+        case Tlv::kLeaderData:
         {
-            LeaderDataTlv leaderData;
+            LeaderDataTlv leaderDataTlv;
 
-            SuccessOrExit(error = aMessage.Read(offset, leaderData));
-            VerifyOrExit(leaderData.IsValid(), error = kErrorParse);
-
-            ParseLeaderData(leaderData, aNetworkDiagTlv.mData.mLeaderData);
+            VerifyOrExit(!tlv.IsExtended(), error = kErrorParse);
+            SuccessOrExit(error = aMessage.Read(offset, leaderDataTlv));
+            VerifyOrExit(leaderDataTlv.IsValid(), error = kErrorParse);
+            leaderDataTlv.Get(AsCoreType(&aTlvInfo.mData.mLeaderData));
             break;
         }
 
-        case NetworkDiagnosticTlv::kNetworkData:
+        case Tlv::kNetworkData:
+            static_assert(sizeof(aTlvInfo.mData.mNetworkData.m8) >= NetworkData::NetworkData::kMaxSize,
+                          "NetworkData array in `otNetworkDiagTlv` is too small");
+
+            VerifyOrExit(tlvLength <= NetworkData::NetworkData::kMaxSize, error = kErrorParse);
+            aTlvInfo.mData.mNetworkData.mCount = static_cast<uint8_t>(tlvLength);
+            aMessage.ReadBytes(valueOffset, aTlvInfo.mData.mNetworkData.m8, tlvLength);
+            break;
+
+        case Tlv::kIp6AddressList:
         {
-            NetworkDataTlv networkData;
+            uint16_t      addrListLength = GetArrayLength(aTlvInfo.mData.mIp6AddrList.mList);
+            Ip6::Address *addrEntry      = AsCoreTypePtr(&aTlvInfo.mData.mIp6AddrList.mList[0]);
+            uint8_t      &addrCount      = aTlvInfo.mData.mIp6AddrList.mCount;
 
-            tlvTotalLength = sizeof(tlv) + tlv.GetLength();
-            VerifyOrExit(tlvTotalLength <= sizeof(networkData), error = kErrorParse);
-            SuccessOrExit(error = aMessage.Read(offset, &networkData, tlvTotalLength));
-            VerifyOrExit(networkData.IsValid(), error = kErrorParse);
-            VerifyOrExit(sizeof(aNetworkDiagTlv.mData.mNetworkData.m8) >= networkData.GetLength(), error = kErrorParse);
+            VerifyOrExit((tlvLength % Ip6::Address::kSize) == 0, error = kErrorParse);
 
-            memcpy(aNetworkDiagTlv.mData.mNetworkData.m8, networkData.GetNetworkData(), networkData.GetLength());
-            aNetworkDiagTlv.mData.mNetworkData.mCount = networkData.GetLength();
-            break;
-        }
+            // `TlvInfo` has a fixed array for IPv6 addresses. If there
+            // are more addresses in the message, we read and return as
+            // many as can fit in array and ignore the rest.
 
-        case NetworkDiagnosticTlv::kIp6AddressList:
-        {
-            Ip6AddressListTlv &ip6AddrList = As<Ip6AddressListTlv>(tlv);
+            addrCount = 0;
 
-            VerifyOrExit(ip6AddrList.IsValid(), error = kErrorParse);
-            VerifyOrExit(sizeof(aNetworkDiagTlv.mData.mIp6AddrList.mList) >= ip6AddrList.GetLength(),
-                         error = kErrorParse);
-            SuccessOrExit(error = aMessage.Read(offset + sizeof(ip6AddrList), aNetworkDiagTlv.mData.mIp6AddrList.mList,
-                                                ip6AddrList.GetLength()));
-            aNetworkDiagTlv.mData.mIp6AddrList.mCount = ip6AddrList.GetLength() / OT_IP6_ADDRESS_SIZE;
-            break;
-        }
-
-        case NetworkDiagnosticTlv::kMacCounters:
-        {
-            MacCountersTlv macCounters;
-
-            SuccessOrExit(error = aMessage.Read(offset, macCounters));
-            VerifyOrExit(macCounters.IsValid(), error = kErrorParse);
-
-            ParseMacCounters(macCounters, aNetworkDiagTlv.mData.mMacCounters);
-            break;
-        }
-
-        case NetworkDiagnosticTlv::kBatteryLevel:
-            SuccessOrExit(error = Tlv::Read<BatteryLevelTlv>(aMessage, offset, aNetworkDiagTlv.mData.mBatteryLevel));
-            break;
-
-        case NetworkDiagnosticTlv::kSupplyVoltage:
-            SuccessOrExit(error = Tlv::Read<SupplyVoltageTlv>(aMessage, offset, aNetworkDiagTlv.mData.mSupplyVoltage));
-            break;
-
-        case NetworkDiagnosticTlv::kChildTable:
-        {
-            ChildTableTlv &childTable = As<ChildTableTlv>(tlv);
-
-            VerifyOrExit(childTable.IsValid(), error = kErrorParse);
-            VerifyOrExit(childTable.GetNumEntries() <= GetArrayLength(aNetworkDiagTlv.mData.mChildTable.mTable),
-                         error = kErrorParse);
-
-            for (uint8_t i = 0; i < childTable.GetNumEntries(); ++i)
+            while ((tlvLength > 0) && (addrCount < addrListLength))
             {
-                ChildTableEntry childEntry;
-                VerifyOrExit(childTable.ReadEntry(childEntry, aMessage, offset, i) == kErrorNone, error = kErrorParse);
-                ParseChildEntry(childEntry, aNetworkDiagTlv.mData.mChildTable.mTable[i]);
+                SuccessOrExit(error = aMessage.Read(valueOffset, *addrEntry));
+                addrCount++;
+                addrEntry++;
+                valueOffset += Ip6::Address::kSize;
+                tlvLength -= Ip6::Address::kSize;
             }
-            aNetworkDiagTlv.mData.mChildTable.mCount = childTable.GetNumEntries();
+
             break;
         }
 
-        case NetworkDiagnosticTlv::kChannelPages:
+        case Tlv::kMacCounters:
         {
-            VerifyOrExit(sizeof(aNetworkDiagTlv.mData.mChannelPages.m8) >= tlv.GetLength(), error = kErrorParse);
-            SuccessOrExit(
-                error = aMessage.Read(offset + sizeof(tlv), aNetworkDiagTlv.mData.mChannelPages.m8, tlv.GetLength()));
-            aNetworkDiagTlv.mData.mChannelPages.mCount = tlv.GetLength();
+            MacCountersTlv macCountersTlv;
+
+            SuccessOrExit(error = aMessage.Read(offset, macCountersTlv));
+            VerifyOrExit(macCountersTlv.IsValid(), error = kErrorParse);
+            ParseMacCounters(macCountersTlv, aTlvInfo.mData.mMacCounters);
             break;
         }
 
-        case NetworkDiagnosticTlv::kMaxChildTimeout:
+        case Tlv::kBatteryLevel:
+            SuccessOrExit(error = Tlv::Read<BatteryLevelTlv>(aMessage, offset, aTlvInfo.mData.mBatteryLevel));
+            break;
+
+        case Tlv::kSupplyVoltage:
+            SuccessOrExit(error = Tlv::Read<SupplyVoltageTlv>(aMessage, offset, aTlvInfo.mData.mSupplyVoltage));
+            break;
+
+        case Tlv::kChildTable:
+        {
+            uint16_t   childInfoLength = GetArrayLength(aTlvInfo.mData.mChildTable.mTable);
+            ChildInfo *childInfo       = &aTlvInfo.mData.mChildTable.mTable[0];
+            uint8_t   &childCount      = aTlvInfo.mData.mChildTable.mCount;
+
+            VerifyOrExit((tlvLength % sizeof(ChildTableEntry)) == 0, error = kErrorParse);
+
+            // `TlvInfo` has a fixed array Child Table entries. If there
+            // are more entries in the message, we read and return as
+            // many as can fit in array and ignore the rest.
+
+            childCount = 0;
+
+            while ((tlvLength > 0) && (childCount < childInfoLength))
+            {
+                ChildTableEntry entry;
+
+                SuccessOrExit(error = aMessage.Read(valueOffset, entry));
+
+                childInfo->mTimeout     = entry.GetTimeout();
+                childInfo->mLinkQuality = entry.GetLinkQuality();
+                childInfo->mChildId     = entry.GetChildId();
+                entry.GetMode().Get(childInfo->mMode);
+
+                childCount++;
+                childInfo++;
+                tlvLength -= sizeof(ChildTableEntry);
+                valueOffset += sizeof(ChildTableEntry);
+            }
+
+            break;
+        }
+
+        case Tlv::kChannelPages:
+            aTlvInfo.mData.mChannelPages.mCount =
+                static_cast<uint8_t>(Min(tlvLength, GetArrayLength(aTlvInfo.mData.mChannelPages.m8)));
+            aMessage.ReadBytes(valueOffset, aTlvInfo.mData.mChannelPages.m8, aTlvInfo.mData.mChannelPages.mCount);
+            break;
+
+        case Tlv::kMaxChildTimeout:
+            SuccessOrExit(error = Tlv::Read<MaxChildTimeoutTlv>(aMessage, offset, aTlvInfo.mData.mMaxChildTimeout));
+            break;
+
+        case Tlv::kVersion:
+            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<MaxChildTimeoutTlv>(aMessage, offset, aNetworkDiagTlv.mData.mMaxChildTimeout));
+                              Tlv::Read<ThreadStackVersionTlv>(aMessage, offset, aTlvInfo.mData.mThreadStackVersion));
             break;
 
         default:
-            // Ignore unrecognized Network Diagnostic TLV silently and
-            // continue to top of the `while(true)` loop.
-            offset += tlv.GetSize();
-            continue;
+            // Skip unrecognized TLVs.
+            skipTlv = true;
+            break;
         }
 
-        // Exit if a TLV is recognized and parsed successfully.
-        aNetworkDiagTlv.mType = tlv.GetType();
-        aIterator             = static_cast<uint16_t>(offset - aMessage.GetOffset() + tlv.GetSize());
-        ExitNow();
+        offset += tlv.GetSize();
+
+        if (!skipTlv)
+        {
+            // 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 *Client::UriToString(Uri aUri)
+{
+    const char *str = "";
+
+    switch (aUri)
+    {
+    case kUriDiagnosticGetQuery:
+        str = ot::UriToString<kUriDiagnosticGetQuery>();
+        break;
+    case kUriDiagnosticGetRequest:
+        str = ot::UriToString<kUriDiagnosticGetRequest>();
+        break;
+    case kUriDiagnosticReset:
+        str = ot::UriToString<kUriDiagnosticReset>();
+        break;
+    default:
+        break;
+    }
+
+    return str;
+}
+
+#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 ade893f..4026aa3 100644
--- a/src/core/thread/network_diagnostic.hpp
+++ b/src/core/thread/network_diagnostic.hpp
@@ -36,15 +36,15 @@
 
 #include "openthread-core-config.h"
 
-#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
-
 #include <openthread/netdiag.h>
 
-#include "coap/coap.hpp"
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "net/udp6.hpp"
 #include "thread/network_diagnostic_tlvs.hpp"
+#include "thread/tmf.hpp"
+#include "thread/uri_paths.hpp"
 
 namespace ot {
 
@@ -59,49 +59,166 @@
  * @{
  */
 
+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 type represents an iterator used to iterate through Network Diagnostic TLVs from `GetNextDiagTlv()`.
+     * This constructor initializes the Server.
+     *
+     * @param[in] aInstance   The OpenThread instance.
      *
      */
-    typedef otNetworkDiagIterator Iterator;
+    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;
+
+public:
+    typedef otNetworkDiagIterator          Iterator;    ///< Iterator to go through TLVs in `GetNextDiagTlv()`.
+    typedef otNetworkDiagTlv               TlvInfo;     ///< Parse info from a Network Diagnostic TLV.
+    typedef otNetworkDiagChildEntry        ChildInfo;   ///< Parsed info for child table entry.
+    typedef otReceiveDiagnosticGetCallback GetCallback; ///< Diagnostic Get callback function pointer type.
 
     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
      * message is sent or the DIAG_GET.req otherwise.
      *
-     * @param[in]  aDestination      A reference to the destination address.
+     * @param[in]  aDestination      The destination address.
      * @param[in]  aTlvTypes         An array of Network Diagnostic TLV types.
-     * @param[in]  aCount            Number of types in aTlvTypes.
-     * @param[in]  aCallback         A pointer to a function that is called when Network Diagnostic Get response
-     *                               is received or NULL to disable the callback.
-     * @param[in]  aCallbackContext  A pointer to application-specific context.
+     * @param[in]  aCount            Number of types in @p aTlvTypes.
+     * @param[in]  aCallback         Callback when Network Diagnostic Get response is received (can be NULL).
+     * @param[in]  Context           Application-specific context used with @p aCallback.
      *
      */
-    Error SendDiagnosticGet(const Ip6::Address &           aDestination,
-                            const uint8_t                  aTlvTypes[],
-                            uint8_t                        aCount,
-                            otReceiveDiagnosticGetCallback aCallback,
-                            void *                         aCallbackContext);
+    Error SendDiagnosticGet(const Ip6::Address &aDestination,
+                            const uint8_t       aTlvTypes[],
+                            uint8_t             aCount,
+                            GetCallback         aCallback,
+                            void               *Context);
 
     /**
      * This method sends Diagnostic Reset request.
      *
-     * @param[in] aDestination  A reference to the destination address.
+     * @param[in] aDestination  The destination address.
      * @param[in] aTlvTypes     An array of Network Diagnostic TLV types.
      * @param[in] aCount        Number of types in aTlvTypes
      *
@@ -111,51 +228,44 @@
     /**
      * This static method gets the next Network Diagnostic TLV in a given message.
      *
-     * @param[in]      aMessage         A message.
-     * @param[in,out]  aIterator        The Network Diagnostic iterator. To get the first TLV set it to
-     *                                  `kIteratorInit`.
-     * @param[out]     aNetworkDiagTlv  A reference to a Network Diagnostic TLV to output the next TLV.
+     * @param[in]      aMessage    Message to read TLVs from.
+     * @param[in,out]  aIterator   The Network Diagnostic iterator. To get the first TLV set it to `kIteratorInit`.
+     * @param[out]     aTlvInfo    A reference to a `TlvInfo` to output the next TLV data.
      *
      * @retval kErrorNone       Successfully found the next Network Diagnostic TLV.
      * @retval kErrorNotFound   No subsequent Network Diagnostic TLV exists in the message.
      * @retval kErrorParse      Parsing the next Network Diagnostic failed.
      *
      */
-    static Error GetNextDiagTlv(const Coap::Message &aMessage, Iterator &aIterator, otNetworkDiagTlv &aNetworkDiagTlv);
+    static Error GetNextDiagTlv(const Coap::Message &aMessage, Iterator &aIterator, TlvInfo &aTlvInfo);
 
 private:
-    Error AppendIp6AddressList(Message &aMessage);
-    Error AppendChildTable(Message &aMessage);
-    void  FillMacCountersTlv(MacCountersTlv &aMacCountersTlv);
-    Error FillRequestedTlvs(const Message &aRequest, Message &aResponse, NetworkDiagnosticTlv &aNetworkDiagnosticTlv);
+    Error SendCommand(Uri                   aUri,
+                      const Ip6::Address   &aDestination,
+                      const uint8_t         aTlvTypes[],
+                      uint8_t               aCount,
+                      Coap::ResponseHandler aHandler = nullptr,
+                      void                 *aContext = nullptr);
 
-    static void HandleDiagnosticGetRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleDiagnosticGetRequest(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    static void HandleGetResponse(void                *aContext,
+                                  otMessage           *aMessage,
+                                  const otMessageInfo *aMessageInfo,
+                                  Error                aResult);
+    void        HandleGetResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult);
 
-    static void HandleDiagnosticGetQuery(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleDiagnosticGetQuery(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
-    static void HandleDiagnosticGetResponse(void *               aContext,
-                                            otMessage *          aMessage,
-                                            const otMessageInfo *aMessageInfo,
-                                            Error                aResult);
-    void HandleDiagnosticGetResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult);
+#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
+    static const char *UriToString(Uri aUri);
+#endif
 
-    static void HandleDiagnosticGetAnswer(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleDiagnosticGetAnswer(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-
-    static void HandleDiagnosticReset(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleDiagnosticReset(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
-
-    Coap::Resource mDiagnosticGetRequest;
-    Coap::Resource mDiagnosticGetQuery;
-    Coap::Resource mDiagnosticGetAnswer;
-    Coap::Resource mDiagnosticReset;
-
-    otReceiveDiagnosticGetCallback mReceiveDiagnosticGetCallback;
-    void *                         mReceiveDiagnosticGetCallbackContext;
+    Callback<GetCallback> mGetCallback;
 };
 
+DeclareTmfHandler(Client, kUriDiagnosticReset);
+
+#endif // OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+
 /**
  * @}
  */
@@ -163,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 8352634..c46298c 100644
--- a/src/core/thread/network_diagnostic_tlvs.hpp
+++ b/src/core/thread/network_diagnostic_tlvs.hpp
@@ -36,65 +36,87 @@
 
 #include "openthread-core-config.h"
 
+#include <openthread/netdiag.h>
 #include <openthread/thread.h>
 
+#include "common/clearable.hpp"
 #include "common/encoding.hpp"
 #include "common/message.hpp"
 #include "common/tlvs.hpp"
 #include "net/ip6_address.hpp"
 #include "radio/radio.hpp"
+#include "thread/link_quality.hpp"
+#include "thread/mle_tlvs.hpp"
 #include "thread/mle_types.hpp"
 
 namespace ot {
-
 namespace NetworkDiagnostic {
 
 using ot::Encoding::BigEndian::HostSwap16;
 using ot::Encoding::BigEndian::HostSwap32;
 
 /**
- * @addtogroup core-mle-tlvs
- *
- * @brief
- *   This module includes definitions for generating and processing MLE TLVs.
- *
- * @{
- *
- */
-
-/**
- * This class implements MLE TLV generation and parsing.
+ * This class implements Network Diagnostic TLV generation and parsing.
  *
  */
 OT_TOOL_PACKED_BEGIN
-class NetworkDiagnosticTlv : public ot::Tlv
+class Tlv : public ot::Tlv
 {
 public:
     /**
-     * MLE TLV Types.
+     * Network Diagnostic TLV Types.
      *
      */
     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,
+        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.
@@ -116,262 +138,133 @@
  * This class defines Extended MAC Address TLV constants and types.
  *
  */
-typedef SimpleTlvInfo<NetworkDiagnosticTlv::kExtMacAddress, Mac::ExtAddress> ExtMacAddressTlv;
+typedef SimpleTlvInfo<Tlv::kExtMacAddress, Mac::ExtAddress> ExtMacAddressTlv;
 
 /**
  * This class defines Address16 TLV constants and types.
  *
  */
-typedef UintTlvInfo<NetworkDiagnosticTlv::kAddress16, uint16_t> Address16Tlv;
+typedef UintTlvInfo<Tlv::kAddress16, uint16_t> Address16Tlv;
 
 /**
  * This class defines Mode TLV constants and types.
  *
  */
-typedef UintTlvInfo<NetworkDiagnosticTlv::kMode, uint8_t> ModeTlv;
+typedef UintTlvInfo<Tlv::kMode, uint8_t> ModeTlv;
 
 /**
  * This class defines Timeout TLV constants and types.
  *
  */
-typedef UintTlvInfo<NetworkDiagnosticTlv::kTimeout, uint32_t> TimeoutTlv;
+typedef UintTlvInfo<Tlv::kTimeout, uint32_t> TimeoutTlv;
+
+/**
+ * This class defines Network Data TLV constants and types.
+ *
+ */
+typedef TlvInfo<Tlv::kNetworkData> NetworkDataTlv;
+
+/**
+ * This class defines IPv6 Address List TLV constants and types.
+ *
+ */
+typedef TlvInfo<Tlv::kIp6AddressList> Ip6AddressListTlv;
 
 /**
  * This class defines Battery Level TLV constants and types.
  *
  */
-typedef UintTlvInfo<NetworkDiagnosticTlv::kBatteryLevel, uint8_t> BatteryLevelTlv;
+typedef UintTlvInfo<Tlv::kBatteryLevel, uint8_t> BatteryLevelTlv;
 
 /**
  * This class defines Supply Voltage TLV constants and types.
  *
  */
-typedef UintTlvInfo<NetworkDiagnosticTlv::kSupplyVoltage, uint16_t> SupplyVoltageTlv;
+typedef UintTlvInfo<Tlv::kSupplyVoltage, uint16_t> SupplyVoltageTlv;
+
+/**
+ * This class defines Child Table TLV constants and types.
+ *
+ */
+typedef TlvInfo<Tlv::kChildTable> ChildTableTlv;
 
 /**
  * This class defines Max Child Timeout TLV constants and types.
  *
  */
-typedef UintTlvInfo<NetworkDiagnosticTlv::kMaxChildTimeout, uint32_t> MaxChildTimeoutTlv;
+typedef UintTlvInfo<Tlv::kMaxChildTimeout, uint32_t> MaxChildTimeoutTlv;
+
+/**
+ * This class defines Version TLV constants and types.
+ *
+ */
+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.
 
 /**
  * This class implements Connectivity TLV generation and parsing.
  *
  */
 OT_TOOL_PACKED_BEGIN
-class ConnectivityTlv : public NetworkDiagnosticTlv, public TlvInfo<NetworkDiagnosticTlv::kConnectivity>
+class ConnectivityTlv : public Mle::ConnectivityTlv
 {
 public:
+    static constexpr uint8_t kType = ot::NetworkDiagnostic::Tlv::kConnectivity; ///< The TLV Type value.
+
     /**
      * This method initializes the TLV.
      *
      */
     void Init(void)
     {
-        SetType(kConnectivity);
-        SetLength(sizeof(*this) - sizeof(NetworkDiagnosticTlv));
+        Mle::ConnectivityTlv::Init();
+        ot::Tlv::SetType(kType);
     }
 
     /**
-     * This method indicates whether or not the TLV appears to be well-formed.
+     * This method retrieves the `Connectivity` value.
      *
-     * @retval TRUE   If the TLV appears to be well-formed.
-     * @retval FALSE  If the TLV does not appear to be well-formed.
+     * @param[out] aConnectivity   A reference to `Connectivity` to populate.
      *
      */
-    bool IsValid(void) const
+    void GetConnectivity(Connectivity &aConnectivity) const
     {
-        return IsSedBufferingIncluded() || (GetLength() == sizeof(*this) - sizeof(NetworkDiagnosticTlv) -
-                                                               sizeof(mSedBufferSize) - sizeof(mSedDatagramCount));
+        aConnectivity.mParentPriority   = GetParentPriority();
+        aConnectivity.mLinkQuality3     = GetLinkQuality3();
+        aConnectivity.mLinkQuality2     = GetLinkQuality2();
+        aConnectivity.mLinkQuality1     = GetLinkQuality1();
+        aConnectivity.mLeaderCost       = GetLeaderCost();
+        aConnectivity.mIdSequence       = GetIdSequence();
+        aConnectivity.mActiveRouters    = GetActiveRouters();
+        aConnectivity.mSedBufferSize    = GetSedBufferSize();
+        aConnectivity.mSedDatagramCount = GetSedDatagramCount();
     }
 
-    /**
-     * This method indicates whether or not the sed buffer size and datagram count are included.
-     *
-     * @retval TRUE   If the sed buffer size and datagram count are included.
-     * @retval FALSE  If the sed buffer size and datagram count are not included.
-     *
-     */
-    bool IsSedBufferingIncluded(void) const { return GetLength() >= sizeof(*this) - sizeof(Tlv); }
-
-    /**
-     * This method returns the Parent Priority value.
-     *
-     * @returns The Parent Priority value.
-     *
-     */
-    int8_t GetParentPriority(void) const { return (mParentPriority & kParentPriorityMask) >> kParentPriorityOffset; }
-
-    /**
-     * This method sets the Parent Priority value.
-     *
-     * @param[in] aParentPriority  The Parent Priority value.
-     *
-     */
-    void SetParentPriority(int8_t aParentPriority)
-    {
-        mParentPriority = (aParentPriority << kParentPriorityOffset) & kParentPriorityMask;
-    }
-
-    /**
-     * This method returns the Link Quality 3 value.
-     *
-     * @returns The Link Quality 3 value.
-     *
-     */
-    uint8_t GetLinkQuality3(void) const { return mLinkQuality3; }
-
-    /**
-     * This method sets the Link Quality 3 value.
-     *
-     * @param[in]  aLinkQuality  The Link Quality 3 value.
-     *
-     */
-    void SetLinkQuality3(uint8_t aLinkQuality) { mLinkQuality3 = aLinkQuality; }
-
-    /**
-     * This method returns the Link Quality 2 value.
-     *
-     * @returns The Link Quality 2 value.
-     *
-     */
-    uint8_t GetLinkQuality2(void) const { return mLinkQuality2; }
-
-    /**
-     * This method sets the Link Quality 2 value.
-     *
-     * @param[in]  aLinkQuality  The Link Quality 2 value.
-     *
-     */
-    void SetLinkQuality2(uint8_t aLinkQuality) { mLinkQuality2 = aLinkQuality; }
-
-    /**
-     * This method sets the Link Quality 1 value.
-     *
-     * @returns The Link Quality 1 value.
-     *
-     */
-    uint8_t GetLinkQuality1(void) const { return mLinkQuality1; }
-
-    /**
-     * This method sets the Link Quality 1 value.
-     *
-     * @param[in]  aLinkQuality  The Link Quality 1 value.
-     *
-     */
-    void SetLinkQuality1(uint8_t aLinkQuality) { mLinkQuality1 = aLinkQuality; }
-
-    /**
-     * This method sets the Active Routers value.
-     *
-     * @returns The Active Routers value.
-     *
-     */
-    uint8_t GetActiveRouters(void) const { return mActiveRouters; }
-
-    /**
-     * This method sets the Active Routers value.
-     *
-     * @param[in]  aActiveRouters  The Active Routers value.
-     *
-     */
-    void SetActiveRouters(uint8_t aActiveRouters) { mActiveRouters = aActiveRouters; }
-
-    /**
-     * This method returns the Leader Cost value.
-     *
-     * @returns The Leader Cost value.
-     *
-     */
-    uint8_t GetLeaderCost(void) const { return mLeaderCost; }
-
-    /**
-     * This method sets the Leader Cost value.
-     *
-     * @param[in]  aCost  The Leader Cost value.
-     *
-     */
-    void SetLeaderCost(uint8_t aCost) { mLeaderCost = aCost; }
-
-    /**
-     * This method returns the ID Sequence value.
-     *
-     * @returns The ID Sequence value.
-     *
-     */
-    uint8_t GetIdSequence(void) const { return mIdSequence; }
-
-    /**
-     * This method sets the ID Sequence value.
-     *
-     * @param[in]  aSequence  The ID Sequence value.
-     *
-     */
-    void SetIdSequence(uint8_t aSequence) { mIdSequence = aSequence; }
-
-    /**
-     * This method returns the SED Buffer Size value.
-     *
-     * @returns The SED Buffer Size value.
-     *
-     */
-    uint16_t GetSedBufferSize(void) const
-    {
-        uint16_t buffersize = OPENTHREAD_CONFIG_DEFAULT_SED_BUFFER_SIZE;
-
-        if (IsSedBufferingIncluded())
-        {
-            buffersize = HostSwap16(mSedBufferSize);
-        }
-        return buffersize;
-    }
-
-    /**
-     * This method sets the SED Buffer Size value.
-     *
-     * @param[in]  aSedBufferSize  The SED Buffer Size value.
-     *
-     */
-    void SetSedBufferSize(uint16_t aSedBufferSize) { mSedBufferSize = HostSwap16(aSedBufferSize); }
-
-    /**
-     * This method returns the SED Datagram Count value.
-     *
-     * @returns The SED Datagram Count value.
-     *
-     */
-    uint8_t GetSedDatagramCount(void) const
-    {
-        uint8_t count = OPENTHREAD_CONFIG_DEFAULT_SED_DATAGRAM_COUNT;
-
-        if (IsSedBufferingIncluded())
-        {
-            count = mSedDatagramCount;
-        }
-        return count;
-    }
-
-    /**
-     * This method sets the SED Datagram Count value.
-     *
-     * @param[in]  aSedDatagramCount  The SED Datagram Count value.
-     *
-     */
-    void SetSedDatagramCount(uint8_t aSedDatagramCount) { mSedDatagramCount = aSedDatagramCount; }
-
-private:
-    static constexpr uint8_t kParentPriorityOffset = 6;
-    static constexpr uint8_t kParentPriorityMask   = 3 << kParentPriorityOffset;
-
-    uint8_t  mParentPriority;
-    uint8_t  mLinkQuality3;
-    uint8_t  mLinkQuality2;
-    uint8_t  mLinkQuality1;
-    uint8_t  mLeaderCost;
-    uint8_t  mIdSequence;
-    uint8_t  mActiveRouters;
-    uint16_t mSedBufferSize;
-    uint8_t  mSedDatagramCount;
 } OT_TOOL_PACKED_END;
 
 /**
@@ -379,165 +272,20 @@
  *
  */
 OT_TOOL_PACKED_BEGIN
-class RouteTlv : public NetworkDiagnosticTlv, public TlvInfo<NetworkDiagnosticTlv::kRoute>
+class RouteTlv : public Mle::RouteTlv
 {
 public:
+    static constexpr uint8_t kType = ot::NetworkDiagnostic::Tlv::kRoute; ///< The TLV Type value.
+
     /**
      * This method initializes the TLV.
      *
      */
     void Init(void)
     {
-        SetType(kRoute);
-        SetLength(sizeof(*this) - sizeof(NetworkDiagnosticTlv));
-        mRouterIdMask.Clear();
+        Mle::RouteTlv::Init();
+        ot::Tlv::SetType(kType);
     }
-
-    /**
-     * This method indicates whether or not the TLV appears to be well-formed.
-     *
-     * @retval TRUE   If the TLV appears to be well-formed.
-     * @retval FALSE  If the TLV does not appear to be well-formed.
-     *
-     */
-    bool IsValid(void) const { return GetLength() >= sizeof(mRouterIdSequence) + sizeof(mRouterIdMask); }
-
-    /**
-     * This method returns the Router ID Sequence value.
-     *
-     * @returns The Router ID Sequence value.
-     *
-     */
-    uint8_t GetRouterIdSequence(void) const { return mRouterIdSequence; }
-
-    /**
-     * This method sets the Router ID Sequence value.
-     *
-     * @param[in]  aSequence  The Router ID Sequence value.
-     *
-     */
-    void SetRouterIdSequence(uint8_t aSequence) { mRouterIdSequence = aSequence; }
-
-    /**
-     * This method indicates whether or not a Router ID bit is set.
-     *
-     * @param[in]  aRouterId  The Router ID.
-     *
-     * @retval TRUE   If the Router ID bit is set.
-     * @retval FALSE  If the Router ID bit is not set.
-     *
-     */
-    bool IsRouterIdSet(uint8_t aRouterId) const { return mRouterIdMask.Contains(aRouterId); }
-
-    /**
-     * This method sets the Router ID bit.
-     *
-     * @param[in]  aRouterId  The Router ID bit to set.
-     *
-     */
-    void SetRouterId(uint8_t aRouterId) { mRouterIdMask.Add(aRouterId); }
-
-    /**
-     * This method returns the Route Data Length value.
-     *
-     * @returns The Route Data Length value.
-     *
-     */
-    uint8_t GetRouteDataLength(void) const { return GetLength() - sizeof(mRouterIdSequence) - sizeof(mRouterIdMask); }
-
-    /**
-     * This method sets the Route Data Length value.
-     *
-     * @param[in]  aLength  The Route Data Length value.
-     *
-     */
-    void SetRouteDataLength(uint8_t aLength) { SetLength(sizeof(mRouterIdSequence) + sizeof(mRouterIdMask) + aLength); }
-
-    /**
-     * This method returns the Route Cost value for a given Router index.
-     *
-     * @param[in]  aRouterIndex  The Router index.
-     *
-     * @returns The Route Cost value for a given Router index.
-     *
-     */
-    uint8_t GetRouteCost(uint8_t aRouterIndex) const { return mRouteData[aRouterIndex] & kRouteCostMask; }
-
-    /**
-     * This method sets the Route Cost value for a given Router index.
-     *
-     * @param[in]  aRouterIndex  The Router index.
-     * @param[in]  aRouteCost    The Route Cost value.
-     *
-     */
-    void SetRouteCost(uint8_t aRouterIndex, uint8_t aRouteCost)
-    {
-        mRouteData[aRouterIndex] = (mRouteData[aRouterIndex] & ~kRouteCostMask) | aRouteCost;
-    }
-
-    /**
-     * This method returns the Link Quality In value for a given Router index.
-     *
-     * @param[in]  aRouterIndex  The Router index.
-     *
-     * @returns The Link Quality In value for a given Router index.
-     *
-     */
-    uint8_t GetLinkQualityIn(uint8_t aRouterIndex) const
-    {
-        return (mRouteData[aRouterIndex] & kLinkQualityInMask) >> kLinkQualityInOffset;
-    }
-
-    /**
-     * This method sets the Link Quality In value for a given Router index.
-     *
-     * @param[in]  aRouterIndex  The Router index.
-     * @param[in]  aLinkQuality  The Link Quality In value for a given Router index.
-     *
-     */
-    void SetLinkQualityIn(uint8_t aRouterIndex, uint8_t aLinkQuality)
-    {
-        mRouteData[aRouterIndex] = (mRouteData[aRouterIndex] & ~kLinkQualityInMask) |
-                                   ((aLinkQuality << kLinkQualityInOffset) & kLinkQualityInMask);
-    }
-
-    /**
-     * This method returns the Link Quality Out value for a given Router index.
-     *
-     * @param[in]  aRouterIndex  The Router index.
-     *
-     * @returns The Link Quality Out value for a given Router index.
-     *
-     */
-    uint8_t GetLinkQualityOut(uint8_t aRouterIndex) const
-    {
-        return (mRouteData[aRouterIndex] & kLinkQualityOutMask) >> kLinkQualityOutOffset;
-    }
-
-    /**
-     * This method sets the Link Quality Out value for a given Router index.
-     *
-     * @param[in]  aRouterIndex  The Router index.
-     * @param[in]  aLinkQuality  The Link Quality Out value for a given Router index.
-     *
-     */
-    void SetLinkQualityOut(uint8_t aRouterIndex, uint8_t aLinkQuality)
-    {
-        mRouteData[aRouterIndex] = (mRouteData[aRouterIndex] & ~kLinkQualityOutMask) |
-                                   ((aLinkQuality << kLinkQualityOutOffset) & kLinkQualityOutMask);
-    }
-
-private:
-    static constexpr uint8_t kLinkQualityOutOffset = 6;
-    static constexpr uint8_t kLinkQualityOutMask   = 3 << kLinkQualityOutOffset;
-    static constexpr uint8_t kLinkQualityInOffset  = 4;
-    static constexpr uint8_t kLinkQualityInMask    = 3 << kLinkQualityInOffset;
-    static constexpr uint8_t kRouteCostOffset      = 0;
-    static constexpr uint8_t kRouteCostMask        = 0xf << kRouteCostOffset;
-
-    uint8_t          mRouterIdSequence;
-    Mle::RouterIdSet mRouterIdMask;
-    uint8_t          mRouteData[Mle::kMaxRouterId + 1];
 } OT_TOOL_PACKED_END;
 
 /**
@@ -545,203 +293,20 @@
  *
  */
 OT_TOOL_PACKED_BEGIN
-class LeaderDataTlv : public NetworkDiagnosticTlv, public TlvInfo<NetworkDiagnosticTlv::kLeaderData>
+class LeaderDataTlv : public Mle::LeaderDataTlv
 {
 public:
+    static constexpr uint8_t kType = ot::NetworkDiagnostic::Tlv::kLeaderData; ///< The TLV Type value.
+
     /**
      * This method initializes the TLV.
      *
      */
     void Init(void)
     {
-        SetType(kLeaderData);
-        SetLength(sizeof(*this) - sizeof(NetworkDiagnosticTlv));
+        Mle::LeaderDataTlv::Init();
+        ot::Tlv::SetType(kType);
     }
-
-    /**
-     * This method indicates whether or not the TLV appears to be well-formed.
-     *
-     * @retval TRUE   If the TLV appears to be well-formed.
-     * @retval FALSE  If the TLV does not appear to be well-formed.
-     *
-     */
-    bool IsValid(void) const { return GetLength() >= sizeof(*this) - sizeof(NetworkDiagnosticTlv); }
-
-    /**
-     * This method returns the Partition ID value.
-     *
-     * @returns The Partition ID value.
-     *
-     */
-    uint32_t GetPartitionId(void) const { return HostSwap32(mPartitionId); }
-
-    /**
-     * This method sets the Partition ID value.
-     *
-     * @param[in]  aPartitionId  The Partition ID value.
-     *
-     */
-    void SetPartitionId(uint32_t aPartitionId) { mPartitionId = HostSwap32(aPartitionId); }
-
-    /**
-     * This method returns the Weighting value.
-     *
-     * @returns The Weighting value.
-     *
-     */
-    uint8_t GetWeighting(void) const { return mWeighting; }
-
-    /**
-     * This method sets the Weighting value.
-     *
-     * @param[in]  aWeighting  The Weighting value.
-     *
-     */
-    void SetWeighting(uint8_t aWeighting) { mWeighting = aWeighting; }
-
-    /**
-     * This method returns the Data Version value.
-     *
-     * @returns The Data Version value.
-     *
-     */
-    uint8_t GetDataVersion(void) const { return mDataVersion; }
-
-    /**
-     * This method sets the Data Version value.
-     *
-     * @param[in]  aVersion  The Data Version value.
-     *
-     */
-    void SetDataVersion(uint8_t aVersion) { mDataVersion = aVersion; }
-
-    /**
-     * This method returns the Stable Data Version value.
-     *
-     * @returns The Stable Data Version value.
-     *
-     */
-    uint8_t GetStableDataVersion(void) const { return mStableDataVersion; }
-
-    /**
-     * This method sets the Stable Data Version value.
-     *
-     * @param[in]  aVersion  The Stable Data Version value.
-     *
-     */
-    void SetStableDataVersion(uint8_t aVersion) { mStableDataVersion = aVersion; }
-
-    /**
-     * This method returns the Leader Router ID value.
-     *
-     * @returns The Leader Router ID value.
-     *
-     */
-    uint8_t GetLeaderRouterId(void) const { return mLeaderRouterId; }
-
-    /**
-     * This method sets the Leader Router ID value.
-     *
-     * @param[in]  aRouterId  The Leader Router ID value.
-     *
-     */
-    void SetLeaderRouterId(uint8_t aRouterId) { mLeaderRouterId = aRouterId; }
-
-private:
-    uint32_t mPartitionId;
-    uint8_t  mWeighting;
-    uint8_t  mDataVersion;
-    uint8_t  mStableDataVersion;
-    uint8_t  mLeaderRouterId;
-} OT_TOOL_PACKED_END;
-
-/**
- * This class implements Network Data TLV generation and parsing.
- *
- */
-OT_TOOL_PACKED_BEGIN
-class NetworkDataTlv : public NetworkDiagnosticTlv, public TlvInfo<NetworkDiagnosticTlv::kNetworkData>
-{
-public:
-    /**
-     * This method initializes the TLV.
-     *
-     */
-    void Init(void)
-    {
-        SetType(kNetworkData);
-        SetLength(sizeof(*this) - sizeof(NetworkDiagnosticTlv));
-    }
-
-    /**
-     * This method indicates whether or not the TLV appears to be well-formed.
-     *
-     * @retval TRUE   If the TLV appears to be well-formed.
-     * @retval FALSE  If the TLV does not appear to be well-formed.
-     *
-     */
-    bool IsValid(void) const { return GetLength() < sizeof(*this) - sizeof(NetworkDiagnosticTlv); }
-
-    /**
-     * This method returns a pointer to the Network Data.
-     *
-     * @returns A pointer to the Network Data.
-     *
-     */
-    uint8_t *GetNetworkData(void) { return mNetworkData; }
-
-    /**
-     * This method sets the Network Data.
-     *
-     * @param[in]  aNetworkData  A pointer to the Network Data.
-     *
-     */
-    void SetNetworkData(const uint8_t *aNetworkData) { memcpy(mNetworkData, aNetworkData, GetLength()); }
-
-private:
-    uint8_t mNetworkData[255];
-} OT_TOOL_PACKED_END;
-
-/**
- * This class implements IPv6 Address List TLV generation and parsing.
- *
- */
-OT_TOOL_PACKED_BEGIN
-class Ip6AddressListTlv : public NetworkDiagnosticTlv, public TlvInfo<NetworkDiagnosticTlv::kIp6AddressList>
-{
-public:
-    /**
-     * This method initializes the TLV.
-     *
-     */
-    void Init(void)
-    {
-        SetType(kIp6AddressList);
-        SetLength(sizeof(*this) - sizeof(NetworkDiagnosticTlv));
-    }
-
-    /**
-     * This method indicates whether or not the TLV appears to be well-formed.
-     *
-     * @retval TRUE   If the TLV appears to be well-formed.
-     * @retval FALSE  If the TLV does not appear to be well-formed.
-     *
-     */
-    bool IsValid(void) const { return !IsExtended() && (GetLength() % sizeof(Ip6::Address) == 0); }
-
-    /**
-     * This method returns a pointer to the IPv6 address entry.
-     *
-     * @param[in]  aIndex  The index into the IPv6 address list.
-     *
-     * @returns A reference to the IPv6 address.
-     *
-     */
-    const Ip6::Address &GetIp6Address(uint8_t aIndex) const
-    {
-        return *reinterpret_cast<const Ip6::Address *>(GetValue() + (aIndex * sizeof(Ip6::Address)));
-    }
-
 } OT_TOOL_PACKED_END;
 
 /**
@@ -749,7 +314,7 @@
  *
  */
 OT_TOOL_PACKED_BEGIN
-class MacCountersTlv : public NetworkDiagnosticTlv, public TlvInfo<NetworkDiagnosticTlv::kMacCounters>
+class MacCountersTlv : public Tlv, public TlvInfo<Tlv::kMacCounters>
 {
 public:
     /**
@@ -759,7 +324,7 @@
     void Init(void)
     {
         SetType(kMacCounters);
-        SetLength(sizeof(*this) - sizeof(NetworkDiagnosticTlv));
+        SetLength(sizeof(*this) - sizeof(Tlv));
     }
 
     /**
@@ -769,7 +334,7 @@
      * @retval FALSE  If the TLV does not appear to be well-formed.
      *
      */
-    bool IsValid(void) const { return GetLength() >= sizeof(*this) - sizeof(NetworkDiagnosticTlv); }
+    bool IsValid(void) const { return GetLength() >= sizeof(*this) - sizeof(Tlv); }
 
     /**
      * This method returns the IfInUnknownProtos counter.
@@ -940,26 +505,16 @@
  *
  */
 OT_TOOL_PACKED_BEGIN
-class ChildTableEntry
+class ChildTableEntry : public Clearable<ChildTableEntry>
 {
 public:
     /**
-     * Default constructor.
-     *
-     */
-    ChildTableEntry(void)
-        : mTimeoutRsvChildId(0)
-        , mMode(0)
-    {
-    }
-
-    /**
      * This method returns the Timeout value.
      *
      * @returns The Timeout value.
      *
      */
-    uint8_t GetTimeout(void) const { return (HostSwap16(mTimeoutRsvChildId) & kTimeoutMask) >> kTimeoutOffset; }
+    uint8_t GetTimeout(void) const { return (GetTimeoutChildId() & kTimeoutMask) >> kTimeoutOffset; }
 
     /**
      * This method sets the Timeout value.
@@ -969,8 +524,29 @@
      */
     void SetTimeout(uint8_t aTimeout)
     {
-        mTimeoutRsvChildId = HostSwap16((HostSwap16(mTimeoutRsvChildId) & ~kTimeoutMask) |
-                                        ((aTimeout << kTimeoutOffset) & kTimeoutMask));
+        SetTimeoutChildId((GetTimeoutChildId() & ~kTimeoutMask) | ((aTimeout << kTimeoutOffset) & kTimeoutMask));
+    }
+
+    /**
+     * This method the Link Quality value.
+     *
+     * @returns The Link Quality value.
+     *
+     */
+    LinkQuality GetLinkQuality(void) const
+    {
+        return static_cast<LinkQuality>((GetTimeoutChildId() & kLqiMask) >> kLqiOffset);
+    }
+
+    /**
+     * This method set the Link Quality value.
+     *
+     * @param[in] aLinkQuality  The Link Quality value.
+     *
+     */
+    void SetLinkQuality(LinkQuality aLinkQuality)
+    {
+        SetTimeoutChildId((GetTimeoutChildId() & ~kLqiMask) | ((aLinkQuality << kLqiOffset) & kLqiMask));
     }
 
     /**
@@ -979,7 +555,7 @@
      * @returns The Child ID value.
      *
      */
-    uint16_t GetChildId(void) const { return HostSwap16(mTimeoutRsvChildId) & kChildIdMask; }
+    uint16_t GetChildId(void) const { return (GetTimeoutChildId() & kChildIdMask) >> kChildIdOffset; }
 
     /**
      * This method sets the Child ID value.
@@ -989,7 +565,7 @@
      */
     void SetChildId(uint16_t aChildId)
     {
-        mTimeoutRsvChildId = HostSwap16((HostSwap16(mTimeoutRsvChildId) & ~kChildIdMask) | (aChildId & kChildIdMask));
+        SetTimeoutChildId((GetTimeoutChildId() & ~kChildIdMask) | ((aChildId << kChildIdOffset) & kChildIdMask));
     }
 
     /**
@@ -1008,114 +584,33 @@
      */
     void SetMode(Mle::DeviceMode aMode) { mMode = aMode.Get(); }
 
-    /**
-     * This method returns the Reserved value.
-     *
-     * @returns The Reserved value.
-     *
-     */
-    uint8_t GetReserved(void) const { return (HostSwap16(mTimeoutRsvChildId) & kReservedMask) >> kReservedOffset; }
-
-    /**
-     * This method sets the Reserved value.
-     *
-     * @param[in]  aReserved  The Reserved value.
-     *
-     */
-    void SetReserved(uint8_t aReserved)
-    {
-        mTimeoutRsvChildId = HostSwap16((HostSwap16(mTimeoutRsvChildId) & ~kReservedMask) |
-                                        ((aReserved << kReservedOffset) & kReservedMask));
-    }
-
 private:
-    static constexpr uint8_t  kTimeoutOffset  = 11;
-    static constexpr uint8_t  kReservedOffset = 9;
-    static constexpr uint16_t kTimeoutMask    = 0xf800;
-    static constexpr uint16_t kReservedMask   = 0x0600;
-    static constexpr uint16_t kChildIdMask    = 0x1ff;
+    //             1                   0
+    //   5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    //  | Timeout |LQI|     Child ID    |
+    //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 
-    uint16_t mTimeoutRsvChildId;
+    static constexpr uint8_t  kTimeoutOffset = 11;
+    static constexpr uint8_t  kLqiOffset     = 9;
+    static constexpr uint8_t  kChildIdOffset = 0;
+    static constexpr uint16_t kTimeoutMask   = 0x1f << kTimeoutOffset;
+    static constexpr uint16_t kLqiMask       = 0x3 << kLqiOffset;
+    static constexpr uint16_t kChildIdMask   = 0x1ff << kChildIdOffset;
+
+    uint16_t GetTimeoutChildId(void) const { return HostSwap16(mTimeoutChildId); }
+    void     SetTimeoutChildId(uint16_t aTimeoutChildIf) { mTimeoutChildId = HostSwap16(aTimeoutChildIf); }
+
+    uint16_t mTimeoutChildId;
     uint8_t  mMode;
 } OT_TOOL_PACKED_END;
 
 /**
- * This class implements Child Table TLV generation and parsing.
- *
- */
-OT_TOOL_PACKED_BEGIN
-class ChildTableTlv : public NetworkDiagnosticTlv, public TlvInfo<NetworkDiagnosticTlv::kChildTable>
-{
-public:
-    /**
-     * This method initializes the TLV.
-     *
-     */
-    void Init(void)
-    {
-        SetType(kChildTable);
-        SetLength(sizeof(*this) - sizeof(NetworkDiagnosticTlv));
-    }
-
-    /**
-     * This method indicates whether or not the TLV appears to be well-formed.
-     *
-     * @retval TRUE   If the TLV appears to be well-formed.
-     * @retval FALSE  If the TLV does not appear to be well-formed.
-     *
-     */
-    bool IsValid(void) const { return (GetLength() % sizeof(ChildTableEntry)) == 0; }
-
-    /**
-     * This method returns the number of Child Table entries.
-     *
-     * @returns The number of Child Table entries.
-     *
-     */
-    uint8_t GetNumEntries(void) const { return GetLength() / sizeof(ChildTableEntry); }
-
-    /**
-     * This method returns the Child Table entry at @p aIndex.
-     *
-     * @param[in]  aIndex  The index into the Child Table list.
-     *
-     * @returns  A reference to the Child Table entry.
-     *
-     */
-    ChildTableEntry &GetEntry(uint16_t aIndex)
-    {
-        return *reinterpret_cast<ChildTableEntry *>(GetValue() + (aIndex * sizeof(ChildTableEntry)));
-    }
-
-    /**
-     * This method reads the Child Table entry at @p aIndex.
-     *
-     * @param[out]  aEntry      A reference to a ChildTableEntry.
-     * @param[in]   aMessage    A reference to the message.
-     * @param[in]   aOffset     The offset of the ChildTableTLV in aMessage.
-     * @param[in]   aIndex      The index into the Child Table list.
-     *
-     * @retval  kErrorNotFound   No such entry is found.
-     * @retval  kErrorNone       Successfully read the entry.
-     *
-     */
-    Error ReadEntry(ChildTableEntry &aEntry, const Message &aMessage, uint16_t aOffset, uint8_t aIndex) const
-    {
-        return ((aIndex < GetNumEntries()) &&
-                (aMessage.Read(aOffset + sizeof(ChildTableTlv) + (aIndex * sizeof(ChildTableEntry)), aEntry) ==
-                 kErrorNone))
-                   ? kErrorNone
-                   : kErrorInvalidArgs;
-    }
-
-} OT_TOOL_PACKED_END;
-
-/**
  * This class implements Channel Pages TLV generation and parsing.
  *
  */
 OT_TOOL_PACKED_BEGIN
-class ChannelPagesTlv : public NetworkDiagnosticTlv, public TlvInfo<NetworkDiagnosticTlv::kChannelPages>
+class ChannelPagesTlv : public Tlv, public TlvInfo<Tlv::kChannelPages>
 {
 public:
     /**
@@ -1125,7 +620,7 @@
     void Init(void)
     {
         SetType(kChannelPages);
-        SetLength(sizeof(*this) - sizeof(NetworkDiagnosticTlv));
+        SetLength(sizeof(*this) - sizeof(Tlv));
     }
 
     /**
@@ -1158,7 +653,7 @@
  *
  */
 OT_TOOL_PACKED_BEGIN
-class TypeListTlv : public NetworkDiagnosticTlv, public TlvInfo<NetworkDiagnosticTlv::kTypeList>
+class TypeListTlv : public Tlv, public TlvInfo<Tlv::kTypeList>
 {
 public:
     /**
@@ -1168,17 +663,11 @@
     void Init(void)
     {
         SetType(kTypeList);
-        SetLength(sizeof(*this) - sizeof(NetworkDiagnosticTlv));
+        SetLength(sizeof(*this) - sizeof(Tlv));
     }
 } OT_TOOL_PACKED_END;
 
-/**
- * @}
- *
- */
-
 } // namespace NetworkDiagnostic
-
 } // namespace ot
 
 #endif // NETWORK_DIAGNOSTIC_TLVS_HPP_
diff --git a/src/core/thread/panid_query_server.cpp b/src/core/thread/panid_query_server.cpp
index 9c57e89..01df10a 100644
--- a/src/core/thread/panid_query_server.cpp
+++ b/src/core/thread/panid_query_server.cpp
@@ -53,22 +53,15 @@
     : InstanceLocator(aInstance)
     , mChannelMask(0)
     , mPanId(Mac::kPanIdBroadcast)
-    , mTimer(aInstance, PanIdQueryServer::HandleTimer)
-    , mPanIdQuery(UriPath::kPanIdQuery, &PanIdQueryServer::HandleQuery, this)
+    , mTimer(aInstance)
 {
-    Get<Tmf::Agent>().AddResource(mPanIdQuery);
 }
 
-void PanIdQueryServer::HandleQuery(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
+template <>
+void PanIdQueryServer::HandleTmf<kUriPanIdQuery>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    static_cast<PanIdQueryServer *>(aContext)->HandleQuery(AsCoapMessage(aMessage), AsCoreType(aMessageInfo));
-}
-
-void PanIdQueryServer::HandleQuery(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);
@@ -82,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:
@@ -115,9 +108,9 @@
     Error                   error = kErrorNone;
     MeshCoP::ChannelMaskTlv channelMask;
     Tmf::MessageInfo        messageInfo(GetInstance());
-    Coap::Message *         message;
+    Coap::Message          *message;
 
-    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(UriPath::kPanIdConflict);
+    message = Get<Tmf::Agent>().NewPriorityConfirmablePostMessage(kUriPanIdConflict);
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     channelMask.Init();
@@ -130,18 +123,13 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("sent panid conflict");
+    LogInfo("Sent %s", UriToString<kUriPanIdConflict>());
 
 exit:
     FreeMessageOnError(message, error);
     MeshCoP::LogError("send panid conflict", error);
 }
 
-void PanIdQueryServer::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<PanIdQueryServer>().HandleTimer();
-}
-
 void PanIdQueryServer::HandleTimer(void)
 {
     IgnoreError(Get<Mac::Mac>().ActiveScan(mChannelMask, 0, HandleScanResult, this));
diff --git a/src/core/thread/panid_query_server.hpp b/src/core/thread/panid_query_server.hpp
index 20f3843..70deaf9 100644
--- a/src/core/thread/panid_query_server.hpp
+++ b/src/core/thread/panid_query_server.hpp
@@ -36,13 +36,13 @@
 
 #include "openthread-core-config.h"
 
-#include "coap/coap.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "common/timer.hpp"
 #include "mac/mac.hpp"
 #include "net/ip6_address.hpp"
 #include "net/udp6.hpp"
+#include "thread/tmf.hpp"
 
 namespace ot {
 
@@ -52,6 +52,8 @@
  */
 class PanIdQueryServer : public InstanceLocator, private NonCopyable
 {
+    friend class Tmf::Agent;
+
 public:
     /**
      * This constructor initializes the object.
@@ -62,28 +64,28 @@
 private:
     static constexpr uint32_t kScanDelay = 1000; ///< SCAN_DELAY (in msec)
 
-    static void HandleQuery(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
-    void        HandleQuery(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
     static void HandleScanResult(Mac::ActiveScanResult *aScanResult, void *aContext);
     void        HandleScanResult(Mac::ActiveScanResult *aScanResult);
 
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void HandleTimer(void);
 
     static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
 
     void SendConflict(void);
 
+    using DelayTimer = TimerMilliIn<PanIdQueryServer, &PanIdQueryServer::HandleTimer>;
+
     Ip6::Address mCommissioner;
     uint32_t     mChannelMask;
     uint16_t     mPanId;
 
-    TimerMilli mTimer;
-
-    Coap::Resource mPanIdQuery;
+    DelayTimer mTimer;
 };
 
+DeclareTmfHandler(PanIdQueryServer, kUriPanIdQuery);
+
 /**
  * @}
  */
diff --git a/src/core/thread/radio_selector.cpp b/src/core/thread/radio_selector.cpp
index cb619a3..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)
@@ -134,7 +134,7 @@
     LogLevel       logLevel  = kLogLevelInfo;
     Mac::RadioType radioType = aFrame.GetRadioType();
     Mac::Address   macDest;
-    Neighbor *     neighbor;
+    Neighbor      *neighbor;
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
     if (radioType == Mac::kRadioTypeTrel)
@@ -257,7 +257,7 @@
 
 Mac::TxFrame &RadioSelector::SelectRadio(Message &aMessage, const Mac::Address &aMacDest, Mac::TxFrames &aTxFrames)
 {
-    Neighbor *      neighbor;
+    Neighbor       *neighbor;
     Mac::RadioType  selectedRadio;
     Mac::RadioTypes selections;
 
@@ -358,7 +358,7 @@
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
 void RadioSelector::Log(LogLevel        aLogLevel,
-                        const char *    aActionText,
+                        const char     *aActionText,
                         Mac::RadioType  aRadioType,
                         const Neighbor &aNeighbor)
 {
@@ -387,9 +387,7 @@
 
 #else // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
-void RadioSelector::Log(LogLevel, const char *, Mac::RadioType, const Neighbor &)
-{
-}
+void RadioSelector::Log(LogLevel, const char *, Mac::RadioType, const Neighbor &) {}
 
 #endif // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
diff --git a/src/core/thread/router_table.cpp b/src/core/thread/router_table.cpp
index 722fadd..407f511 100644
--- a/src/core/thread/router_table.cpp
+++ b/src/core/thread/router_table.cpp
@@ -44,61 +44,32 @@
 
 RegisterLogModule("RouterTable");
 
-RouterTable::Iterator::Iterator(Instance &aInstance)
-    : InstanceLocator(aInstance)
-    , ItemPtrIterator(Get<RouterTable>().GetFirstEntry())
-{
-}
-
-void RouterTable::Iterator::Advance(void)
-{
-    mItem = Get<RouterTable>().GetNextEntry(mItem);
-}
-
 RouterTable::RouterTable(Instance &aInstance)
     : InstanceLocator(aInstance)
+    , mRouters(aInstance)
+    , mChangedTask(aInstance)
     , mRouterIdSequenceLastUpdated(0)
     , mRouterIdSequence(Random::NonCrypto::GetUint8())
-    , mActiveRouterCount(0)
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     , mMinRouterId(0)
     , mMaxRouterId(Mle::kMaxRouterId)
 #endif
 {
-    for (Router &router : mRouters)
-    {
-        router.Init(aInstance);
-    }
-
     Clear();
 }
 
-const Router *RouterTable::GetFirstEntry(void) const
-{
-    const Router *router = &mRouters[0];
-    VerifyOrExit(router->GetRloc16() != 0xffff, router = nullptr);
-
-exit:
-    return router;
-}
-
-const Router *RouterTable::GetNextEntry(const Router *aRouter) const
-{
-    VerifyOrExit(aRouter != nullptr);
-    aRouter++;
-    VerifyOrExit(aRouter < &mRouters[Mle::kMaxRouters], aRouter = nullptr);
-    VerifyOrExit(aRouter->GetRloc16() != 0xffff, aRouter = nullptr);
-
-exit:
-    return aRouter;
-}
-
 void RouterTable::Clear(void)
 {
     ClearNeighbors();
-    mAllocatedRouterIds.Clear();
-    memset(mRouterIdReuseDelay, 0, sizeof(mRouterIdReuseDelay));
-    UpdateAllocation();
+    mRouterIdMap.Clear();
+    mRouters.Clear();
+    SignalTableChanged();
+}
+
+bool RouterTable::IsRouteTlvIdSequenceMoreRecent(const Mle::RouteTlv &aRouteTlv) const
+{
+    return (GetActiveRouterCount() == 0) ||
+           SerialNumber::IsGreater(aRouteTlv.GetRouterIdSequence(), GetRouterIdSequence());
 }
 
 void RouterTable::ClearNeighbors(void)
@@ -108,163 +79,107 @@
         if (router.IsStateValid())
         {
             Get<NeighborTable>().Signal(NeighborTable::kRouterRemoved, router);
+            SignalTableChanged();
         }
 
         router.SetState(Neighbor::kStateInvalid);
     }
 }
 
-bool RouterTable::IsAllocated(uint8_t aRouterId) const
+Router *RouterTable::AddRouter(uint8_t aRouterId)
 {
-    return mAllocatedRouterIds.Contains(aRouterId);
+    // Add a new `Router` entry to `mRouters` array with given
+    // `aRouterId` and update the `mRouterIdMap`.
+
+    Router *router = mRouters.PushBack();
+
+    VerifyOrExit(router != nullptr);
+
+    router->Clear();
+    router->SetRloc16(Mle::Rloc16FromRouterId(aRouterId));
+    router->SetNextHopToInvalid();
+
+    mRouterIdMap.SetIndex(aRouterId, mRouters.IndexOf(*router));
+    SignalTableChanged();
+
+exit:
+    return router;
 }
 
-void RouterTable::UpdateAllocation(void)
+void RouterTable::RemoveRouter(Router &aRouter)
 {
-    uint8_t indexMap[Mle::kMaxRouterId + 1];
+    // Remove an existing `aRouter` entry from `mRouters` and update the
+    // `mRouterIdMap`.
 
-    mActiveRouterCount = 0;
-
-    // build index map
-    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+    if (aRouter.IsStateValid())
     {
-        if (IsAllocated(routerId) && mActiveRouterCount < Mle::kMaxRouters)
-        {
-            indexMap[routerId] = mActiveRouterCount++;
-        }
-        else
-        {
-            indexMap[routerId] = Mle::kInvalidRouterId;
-        }
+        Get<NeighborTable>().Signal(NeighborTable::kRouterRemoved, aRouter);
     }
 
-    // shift entries forward
-    for (int index = Mle::kMaxRouters - 2; index >= 0; index--)
+    mRouterIdMap.Release(aRouter.GetRouterId());
+    mRouters.Remove(aRouter);
+
+    // Removing `aRouter` from `mRouters` array will replace it with
+    // the last entry in the array (if not already the last entry) so
+    // we update the index in `mRouteIdMap` for the moved entry.
+
+    if (IsAllocated(aRouter.GetRouterId()))
     {
-        uint8_t routerId = mRouters[index].GetRouterId();
-        uint8_t newIndex;
-
-        if (routerId > Mle::kMaxRouterId || indexMap[routerId] == Mle::kInvalidRouterId)
-        {
-            continue;
-        }
-
-        newIndex = indexMap[routerId];
-
-        if (newIndex > index)
-        {
-            mRouters[newIndex] = mRouters[index];
-        }
+        mRouterIdMap.SetIndex(aRouter.GetRouterId(), mRouters.IndexOf((aRouter)));
     }
 
-    // shift entries backward
-    for (uint8_t index = 1; index < Mle::kMaxRouters; index++)
-    {
-        uint8_t routerId = mRouters[index].GetRouterId();
-        uint8_t newIndex;
-
-        if (routerId > Mle::kMaxRouterId || indexMap[routerId] == Mle::kInvalidRouterId)
-        {
-            continue;
-        }
-
-        newIndex = indexMap[routerId];
-
-        if (newIndex < index)
-        {
-            mRouters[newIndex] = mRouters[index];
-        }
-    }
-
-    // fix replaced entries
-    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
-    {
-        uint8_t index = indexMap[routerId];
-
-        if (index != Mle::kInvalidRouterId)
-        {
-            Router &router = mRouters[index];
-
-            if (router.GetRouterId() != routerId)
-            {
-                router.Clear();
-                router.SetRloc16(Mle::Mle::Rloc16FromRouterId(routerId));
-                router.SetNextHop(Mle::kInvalidRouterId);
-            }
-        }
-    }
-
-    // clear unused entries
-    for (uint8_t index = mActiveRouterCount; index < Mle::kMaxRouters; index++)
-    {
-        Router &router = mRouters[index];
-        router.Clear();
-        router.SetRloc16(0xffff);
-    }
+    SignalTableChanged();
 }
 
 Router *RouterTable::Allocate(void)
 {
-    Router *rval         = nullptr;
-    uint8_t numAvailable = 0;
-    uint8_t freeBit;
+    Router *router           = nullptr;
+    uint8_t numAvailable     = 0;
+    uint8_t selectedRouterId = Mle::kInvalidRouterId;
 
-    // count available router ids
+    VerifyOrExit(!mRouters.IsFull());
+
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     for (uint8_t routerId = mMinRouterId; routerId <= mMaxRouterId; routerId++)
 #else
     for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
 #endif
     {
-        if (!IsAllocated(routerId) && mRouterIdReuseDelay[routerId] == 0)
+        if (mRouterIdMap.CanAllocate(routerId))
         {
             numAvailable++;
+
+            // Randomly select a router ID as we iterate through the
+            // list using Reservoir algorithm: We replace the
+            // selected ID with current entry in the list with
+            // probably `1/numAvailable`.
+
+            if (Random::NonCrypto::GetUint8InRange(0, numAvailable) == 0)
+            {
+                selectedRouterId = routerId;
+            }
         }
     }
 
-    VerifyOrExit(mActiveRouterCount < Mle::kMaxRouters && numAvailable > 0);
+    VerifyOrExit(selectedRouterId != Mle::kInvalidRouterId);
 
-    // choose available router id at random
-    freeBit = Random::NonCrypto::GetUint8InRange(0, numAvailable);
-
-    // allocate router
-#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-    for (uint8_t routerId = mMinRouterId; routerId <= mMaxRouterId; routerId++)
-#else
-    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
-#endif
-    {
-        if (IsAllocated(routerId) || mRouterIdReuseDelay[routerId] > 0)
-        {
-            continue;
-        }
-
-        if (freeBit == 0)
-        {
-            rval = Allocate(routerId);
-            OT_ASSERT(rval != nullptr);
-            ExitNow();
-        }
-
-        freeBit--;
-    }
+    router = Allocate(selectedRouterId);
+    OT_ASSERT(router != nullptr);
 
 exit:
-    return rval;
+    return router;
 }
 
 Router *RouterTable::Allocate(uint8_t aRouterId)
 {
-    Router *rval = nullptr;
+    Router *router = nullptr;
 
-    VerifyOrExit(aRouterId <= Mle::kMaxRouterId && mActiveRouterCount < Mle::kMaxRouters && !IsAllocated(aRouterId) &&
-                 mRouterIdReuseDelay[aRouterId] == 0);
+    VerifyOrExit(aRouterId <= Mle::kMaxRouterId && mRouterIdMap.CanAllocate(aRouterId));
 
-    mAllocatedRouterIds.Add(aRouterId);
-    UpdateAllocation();
+    router = AddRouter(aRouterId);
+    VerifyOrExit(router != nullptr);
 
-    rval = GetRouter(aRouterId);
-    rval->SetLastHeard(TimerMilli::GetNow());
+    router->SetLastHeard(TimerMilli::GetNow());
 
     mRouterIdSequence++;
     mRouterIdSequenceLastUpdated = TimerMilli::GetNow();
@@ -273,46 +188,37 @@
     LogNote("Allocate router id %d", aRouterId);
 
 exit:
-    return rval;
+    return router;
 }
 
 Error RouterTable::Release(uint8_t aRouterId)
 {
-    Error    error  = kErrorNone;
-    uint16_t rloc16 = Mle::Mle::Rloc16FromRouterId(aRouterId);
-    Router * router;
+    Error   error = kErrorNone;
+    Router *router;
 
     OT_ASSERT(aRouterId <= Mle::kMaxRouterId);
 
     VerifyOrExit(Get<Mle::MleRouter>().IsLeader(), error = kErrorInvalidState);
-    VerifyOrExit(IsAllocated(aRouterId), error = kErrorNotFound);
 
-    router = GetNeighbor(rloc16);
+    router = FindRouterById(aRouterId);
+    VerifyOrExit(router != nullptr, error = kErrorNotFound);
 
-    if (router != nullptr)
+    RemoveRouter(*router);
+
+    for (Router &otherRouter : mRouters)
     {
-        Get<NeighborTable>().Signal(NeighborTable::kRouterRemoved, *router);
-    }
-
-    mAllocatedRouterIds.Remove(aRouterId);
-    UpdateAllocation();
-
-    mRouterIdReuseDelay[aRouterId] = Mle::kRouterIdReuseDelay;
-
-    for (router = GetFirstEntry(); router != nullptr; router = GetNextEntry(router))
-    {
-        if (router->GetNextHop() == rloc16)
+        if (otherRouter.GetNextHop() == aRouterId)
         {
-            router->SetNextHop(Mle::kInvalidRouterId);
-            router->SetCost(0);
+            otherRouter.SetNextHopToInvalid();
         }
     }
 
     mRouterIdSequence++;
     mRouterIdSequenceLastUpdated = TimerMilli::GetNow();
 
-    Get<AddressResolver>().Remove(aRouterId);
-    Get<NetworkData::Leader>().RemoveBorderRouter(rloc16, NetworkData::Leader::kMatchModeRouterId);
+    Get<AddressResolver>().RemoveEntriesForRouterId(aRouterId);
+    Get<NetworkData::Leader>().RemoveBorderRouter(Mle::Rloc16FromRouterId(aRouterId),
+                                                  NetworkData::Leader::kMatchModeRouterId);
     Get<Mle::MleRouter>().ResetAdvertiseInterval();
 
     LogNote("Release router id %d", aRouterId);
@@ -323,20 +229,21 @@
 
 void RouterTable::RemoveRouterLink(Router &aRouter)
 {
-    if (aRouter.GetLinkQualityOut() != 0)
+    if (aRouter.GetLinkQualityOut() != kLinkQuality0)
     {
         aRouter.SetLinkQualityOut(kLinkQuality0);
         aRouter.SetLastHeard(TimerMilli::GetNow());
+        SignalTableChanged();
     }
 
-    for (Router *cur = GetFirstEntry(); cur != nullptr; cur = GetNextEntry(cur))
+    for (Router &router : mRouters)
     {
-        if (cur->GetNextHop() == aRouter.GetRouterId())
+        if (router.GetNextHop() == aRouter.GetRouterId())
         {
-            cur->SetNextHop(Mle::kInvalidRouterId);
-            cur->SetCost(0);
+            router.SetNextHopToInvalid();
+            SignalTableChanged();
 
-            if (GetLinkCost(*cur) >= Mle::kMaxRouteCost)
+            if (GetLinkCost(router) >= Mle::kMaxRouteCost)
             {
                 Get<Mle::MleRouter>().ResetAdvertiseInterval();
             }
@@ -348,41 +255,16 @@
         Get<Mle::MleRouter>().ResetAdvertiseInterval();
 
         // Clear all EID-to-RLOC entries associated with the router.
-        Get<AddressResolver>().Remove(aRouter.GetRouterId());
+        Get<AddressResolver>().RemoveEntriesForRouterId(aRouter.GetRouterId());
     }
 }
 
-uint8_t RouterTable::GetActiveLinkCount(void) const
-{
-    uint8_t activeLinks = 0;
-
-    for (const Router *router = GetFirstEntry(); router != nullptr; router = GetNextEntry(router))
-    {
-        if (router->IsStateValid())
-        {
-            activeLinks++;
-        }
-    }
-
-    return activeLinks;
-}
-
 const Router *RouterTable::FindRouter(const Router::AddressMatcher &aMatcher) const
 {
-    const Router *router;
-
-    for (router = GetFirstEntry(); router != nullptr; router = GetNextEntry(router))
-    {
-        if (router->Matches(aMatcher))
-        {
-            break;
-        }
-    }
-
-    return router;
+    return mRouters.FindMatching(aMatcher);
 }
 
-Router *RouterTable::GetNeighbor(uint16_t aRloc16)
+Router *RouterTable::FindNeighbor(uint16_t aRloc16)
 {
     Router *router = nullptr;
 
@@ -393,32 +275,37 @@
     return router;
 }
 
-Router *RouterTable::GetNeighbor(const Mac::ExtAddress &aExtAddress)
+Router *RouterTable::FindNeighbor(const Mac::ExtAddress &aExtAddress)
 {
     return FindRouter(Router::AddressMatcher(aExtAddress, Router::kInStateValid));
 }
 
-Router *RouterTable::GetNeighbor(const Mac::Address &aMacAddress)
+Router *RouterTable::FindNeighbor(const Mac::Address &aMacAddress)
 {
     return FindRouter(Router::AddressMatcher(aMacAddress, Router::kInStateValid));
 }
 
-const Router *RouterTable::GetRouter(uint8_t aRouterId) const
+const Router *RouterTable::FindRouterById(uint8_t aRouterId) const
 {
     const Router *router = nullptr;
-    uint16_t      rloc16;
 
-    // Skip if invalid router id is passed.
-    VerifyOrExit(aRouterId < Mle::kInvalidRouterId);
+    VerifyOrExit(aRouterId <= Mle::kMaxRouterId);
 
-    rloc16 = Mle::Mle::Rloc16FromRouterId(aRouterId);
-    router = FindRouter(Router::AddressMatcher(rloc16, Router::kInStateAny));
+    VerifyOrExit(IsAllocated(aRouterId));
+    router = &mRouters[mRouterIdMap.GetIndex(aRouterId)];
 
 exit:
     return router;
 }
 
-Router *RouterTable::GetRouter(const Mac::ExtAddress &aExtAddress)
+const Router *RouterTable::FindRouterByRloc16(uint16_t aRloc16) const
+{
+    return FindRouterById(Mle::RouterIdFromRloc16(aRloc16));
+}
+
+const Router *RouterTable::FindNextHopOf(const Router &aRouter) const { return FindRouterById(aRouter.GetNextHop()); }
+
+Router *RouterTable::FindRouter(const Mac::ExtAddress &aExtAddress)
 {
     return FindRouter(Router::AddressMatcher(aExtAddress, Router::kInStateAny));
 }
@@ -435,12 +322,12 @@
     }
     else
     {
-        VerifyOrExit(Mle::Mle::IsActiveRouter(aRouterId), error = kErrorInvalidArgs);
-        routerId = Mle::Mle::RouterIdFromRloc16(aRouterId);
+        VerifyOrExit(Mle::IsActiveRouter(aRouterId), error = kErrorInvalidArgs);
+        routerId = Mle::RouterIdFromRloc16(aRouterId);
         VerifyOrExit(routerId <= Mle::kMaxRouterId, error = kErrorInvalidArgs);
     }
 
-    router = GetRouter(routerId);
+    router = FindRouterById(routerId);
     VerifyOrExit(router != nullptr, error = kErrorNotFound);
 
     aRouterInfo.SetFrom(*router);
@@ -449,23 +336,20 @@
     return error;
 }
 
-Router *RouterTable::GetLeader(void)
-{
-    return GetRouter(Get<Mle::MleRouter>().GetLeaderId());
-}
+const Router *RouterTable::GetLeader(void) const { return FindRouterById(Get<Mle::MleRouter>().GetLeaderId()); }
 
 uint32_t RouterTable::GetLeaderAge(void) const
 {
-    return (mActiveRouterCount > 0) ? Time::MsecToSec(TimerMilli::GetNow() - mRouterIdSequenceLastUpdated) : 0xffffffff;
+    return (!mRouters.IsEmpty()) ? Time::MsecToSec(TimerMilli::GetNow() - mRouterIdSequenceLastUpdated) : 0xffffffff;
 }
 
 uint8_t RouterTable::GetNeighborCount(void) const
 {
     uint8_t count = 0;
 
-    for (const Router *router = GetFirstEntry(); router != nullptr; router = GetNextEntry(router))
+    for (const Router &router : mRouters)
     {
-        if (router->IsStateValid())
+        if (router.IsStateValid())
         {
             count++;
         }
@@ -474,76 +358,468 @@
     return count;
 }
 
-uint8_t RouterTable::GetLinkCost(Router &aRouter)
+uint8_t RouterTable::GetLinkCost(const Router &aRouter) const
 {
     uint8_t rval = Mle::kMaxRouteCost;
 
     VerifyOrExit(aRouter.GetRloc16() != Get<Mle::MleRouter>().GetRloc16() && aRouter.IsStateValid());
 
-    rval = aRouter.GetLinkInfo().GetLinkQuality();
-
-    if (rval > aRouter.GetLinkQualityOut())
-    {
-        rval = aRouter.GetLinkQualityOut();
-    }
-
-    rval = Mle::MleRouter::LinkQualityToCost(rval);
+    rval = CostForLinkQuality(aRouter.GetTwoWayLinkQuality());
 
 exit:
     return rval;
 }
 
-void RouterTable::UpdateRouterIdSet(uint8_t aRouterIdSequence, const Mle::RouterIdSet &aRouterIdSet)
+uint8_t RouterTable::GetLinkCost(uint8_t aRouterId) const
 {
-    mRouterIdSequence            = aRouterIdSequence;
-    mRouterIdSequenceLastUpdated = TimerMilli::GetNow();
+    uint8_t       rval = Mle::kMaxRouteCost;
+    const Router *router;
 
-    VerifyOrExit(mAllocatedRouterIds != aRouterIdSet);
+    router = FindRouterById(aRouterId);
 
-    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+    // `nullptr` aRouterId indicates non-existing next hop, hence return kMaxRouteCost for it.
+    VerifyOrExit(router != nullptr);
+
+    rval = GetLinkCost(*router);
+
+exit:
+    return rval;
+}
+
+uint8_t RouterTable::GetPathCost(uint16_t aDestRloc16) const
+{
+    uint8_t  pathCost;
+    uint16_t nextHopRloc16;
+
+    GetNextHopAndPathCost(aDestRloc16, nextHopRloc16, pathCost);
+
+    return pathCost;
+}
+
+uint8_t RouterTable::GetPathCostToLeader(void) const
+{
+    return GetPathCost(Mle::Rloc16FromRouterId(Get<Mle::Mle>().GetLeaderId()));
+}
+
+void RouterTable::GetNextHopAndPathCost(uint16_t aDestRloc16, uint16_t &aNextHopRloc16, uint8_t &aPathCost) const
+{
+    uint8_t       destRouterId;
+    const Router *router;
+    const Router *nextHop;
+
+    aPathCost      = Mle::kMaxRouteCost;
+    aNextHopRloc16 = Mle::kInvalidRloc16;
+
+    VerifyOrExit(Get<Mle::Mle>().IsAttached());
+
+    if (aDestRloc16 == Get<Mle::Mle>().GetRloc16())
     {
-        // If was allocated but removed in new Router Id Set
-        if (IsAllocated(routerId) && !aRouterIdSet.Contains(routerId))
+        // Destination is this device, return cost as zero.
+        aPathCost      = 0;
+        aNextHopRloc16 = aDestRloc16;
+        ExitNow();
+    }
+
+    destRouterId = Mle::RouterIdFromRloc16(aDestRloc16);
+
+    router  = FindRouterById(destRouterId);
+    nextHop = (router != nullptr) ? FindNextHopOf(*router) : nullptr;
+
+    if (Get<Mle::MleRouter>().IsChild())
+    {
+        const Router &parent = Get<Mle::Mle>().GetParent();
+
+        if (parent.IsStateValid())
         {
-            Router *router = GetRouter(routerId);
+            aNextHopRloc16 = parent.GetRloc16();
+        }
 
-            OT_ASSERT(router != nullptr);
-            router->SetNextHop(Mle::kInvalidRouterId);
-            RemoveRouterLink(*router);
+        // If destination is our parent or another child of our
+        // parent, we use the link cost to our parent. Otherwise we
+        // check if we have a next hop towards the destination and
+        // add its cost to the link cost to parent.
 
-            mAllocatedRouterIds.Remove(routerId);
+        VerifyOrExit((destRouterId == parent.GetRouterId()) || (nextHop != nullptr));
+
+        aPathCost = CostForLinkQuality(parent.GetLinkQualityIn());
+
+        if (destRouterId != parent.GetRouterId())
+        {
+            aPathCost += router->GetCost();
+        }
+
+        // The case where destination itself is a child is handled at
+        // the end (after `else` block).
+    }
+    else // Role is router or leader
+    {
+        if (destRouterId == Mle::RouterIdFromRloc16(Get<Mle::Mle>().GetRloc16()))
+        {
+            // Destination is a one of our children.
+
+            const Child *child = Get<ChildTable>().FindChild(aDestRloc16, Child::kInStateAnyExceptInvalid);
+
+            VerifyOrExit(child != nullptr);
+            aNextHopRloc16 = aDestRloc16;
+            aPathCost      = CostForLinkQuality(child->GetLinkQualityIn());
+            ExitNow();
+        }
+
+        VerifyOrExit(router != nullptr);
+
+        aPathCost = GetLinkCost(*router);
+
+        if (aPathCost < Mle::kMaxRouteCost)
+        {
+            aNextHopRloc16 = router->GetRloc16();
+        }
+
+        if (nextHop != nullptr)
+        {
+            // Determine whether direct link or forwarding hop link
+            // through `nextHop` has a lower path cost.
+
+            uint8_t nextHopPathCost = router->GetCost() + GetLinkCost(*nextHop);
+
+            if (nextHopPathCost < aPathCost)
+            {
+                aPathCost      = nextHopPathCost;
+                aNextHopRloc16 = nextHop->GetRloc16();
+            }
         }
     }
 
-    mAllocatedRouterIds = aRouterIdSet;
-    UpdateAllocation();
+    if (!Mle::IsActiveRouter(aDestRloc16))
+    {
+        // Destination is a child. we assume best link quality
+        // between destination and its parent router.
+
+        aPathCost += kCostForLinkQuality3;
+    }
+
+exit:
+    return;
+}
+
+uint16_t RouterTable::GetNextHop(uint16_t aDestRloc16) const
+{
+    uint8_t  pathCost;
+    uint16_t nextHopRloc16;
+
+    GetNextHopAndPathCost(aDestRloc16, nextHopRloc16, pathCost);
+
+    return nextHopRloc16;
+}
+
+void RouterTable::UpdateRouterIdSet(uint8_t aRouterIdSequence, const Mle::RouterIdSet &aRouterIdSet)
+{
+    bool shouldAdd = false;
+
+    mRouterIdSequence            = aRouterIdSequence;
+    mRouterIdSequenceLastUpdated = TimerMilli::GetNow();
+
+    // Remove all previously allocated routers that are now removed in
+    // new `aRouterIdSet`.
+
+    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+    {
+        if (IsAllocated(routerId) == aRouterIdSet.Contains(routerId))
+        {
+            continue;
+        }
+
+        if (IsAllocated(routerId))
+        {
+            Router *router = FindRouterById(routerId);
+
+            OT_ASSERT(router != nullptr);
+            router->SetNextHopToInvalid();
+            RemoveRouterLink(*router);
+            RemoveRouter(*router);
+        }
+        else
+        {
+            shouldAdd = true;
+        }
+    }
+
+    VerifyOrExit(shouldAdd);
+
+    // Now add all new routers in `aRouterIdSet`.
+
+    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+    {
+        if (!IsAllocated(routerId) && aRouterIdSet.Contains(routerId))
+        {
+            AddRouter(routerId);
+        }
+    }
+
     Get<Mle::MleRouter>().ResetAdvertiseInterval();
 
 exit:
     return;
 }
 
-void RouterTable::HandleTimeTick(void)
+void RouterTable::UpdateRoutes(const Mle::RouteTlv &aRouteTlv, uint8_t aNeighborId)
 {
-    Mle::MleRouter &mle = Get<Mle::MleRouter>();
+    Router          *neighbor;
+    Mle::RouterIdSet finitePathCostIdSet;
+    uint8_t          linkCostToNeighbor;
 
-    if (mle.IsLeader())
+    neighbor = FindRouterById(aNeighborId);
+    VerifyOrExit(neighbor != nullptr);
+
+    // Before updating the routes, we track which routers have finite
+    // path cost. After the update we check again to see if any path
+    // cost changed from finite to infinite or vice versa to decide
+    // whether to reset the  MLE Advertisement interval.
+
+    finitePathCostIdSet.Clear();
+
+    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
     {
-        // update router id sequence
-        if (GetLeaderAge() >= Mle::kRouterIdSequencePeriod)
+        if (GetPathCost(Mle::Rloc16FromRouterId(routerId)) < Mle::kMaxRouteCost)
         {
-            mRouterIdSequence++;
-            mRouterIdSequenceLastUpdated = TimerMilli::GetNow();
+            finitePathCostIdSet.Add(routerId);
+        }
+    }
+
+    // Find the entry corresponding to our Router ID in the received
+    // `aRouteTlv` to get the `LinkQualityIn` from the perspective of
+    // neighbor. We use this to update our `LinkQualityOut` to the
+    // neighbor.
+
+    for (uint8_t routerId = 0, index = 0; routerId <= Mle::kMaxRouterId;
+         index += aRouteTlv.IsRouterIdSet(routerId) ? 1 : 0, routerId++)
+    {
+        if (routerId != Mle::RouterIdFromRloc16(Get<Mle::Mle>().GetRloc16()))
+        {
+            continue;
         }
 
-        for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+        if (aRouteTlv.IsRouterIdSet(routerId))
         {
-            if (mRouterIdReuseDelay[routerId] > 0)
+            LinkQuality linkQuality = aRouteTlv.GetLinkQualityIn(index);
+
+            if (neighbor->GetLinkQualityOut() != linkQuality)
             {
-                mRouterIdReuseDelay[routerId]--;
+                neighbor->SetLinkQualityOut(linkQuality);
+                SignalTableChanged();
+            }
+        }
+
+        break;
+    }
+
+    linkCostToNeighbor = GetLinkCost(*neighbor);
+
+    for (uint8_t routerId = 0, index = 0; routerId <= Mle::kMaxRouterId;
+         index += aRouteTlv.IsRouterIdSet(routerId) ? 1 : 0, routerId++)
+    {
+        Router *router;
+        Router *nextHop;
+        uint8_t cost;
+
+        if (!aRouteTlv.IsRouterIdSet(routerId))
+        {
+            continue;
+        }
+
+        router = FindRouterById(routerId);
+
+        if (router == nullptr || router->GetRloc16() == Get<Mle::Mle>().GetRloc16() || router == neighbor)
+        {
+            continue;
+        }
+
+        nextHop = FindNextHopOf(*router);
+
+        cost = aRouteTlv.GetRouteCost(index);
+        cost = (cost == 0) ? Mle::kMaxRouteCost : cost;
+
+        if ((nextHop == nullptr) || (nextHop == neighbor))
+        {
+            // `router` has no next hop or next hop is neighbor (sender)
+
+            if (cost + linkCostToNeighbor < Mle::kMaxRouteCost)
+            {
+                if (router->SetNextHopAndCost(aNeighborId, cost))
+                {
+                    SignalTableChanged();
+                }
+            }
+            else if (nextHop == neighbor)
+            {
+                router->SetNextHopToInvalid();
+                router->SetLastHeard(TimerMilli::GetNow());
+                SignalTableChanged();
+            }
+        }
+        else
+        {
+            uint8_t curCost = router->GetCost() + GetLinkCost(*nextHop);
+            uint8_t newCost = cost + linkCostToNeighbor;
+
+            if (newCost < curCost)
+            {
+                router->SetNextHopAndCost(aNeighborId, cost);
+                SignalTableChanged();
             }
         }
     }
+
+    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+    {
+        bool oldCostFinite = finitePathCostIdSet.Contains(routerId);
+        bool newCostFinite = (GetPathCost(Mle::Rloc16FromRouterId(routerId)) < Mle::kMaxRouteCost);
+
+        if (newCostFinite != oldCostFinite)
+        {
+            Get<Mle::MleRouter>().ResetAdvertiseInterval();
+            break;
+        }
+    }
+
+exit:
+    return;
+}
+
+void RouterTable::UpdateRoutesOnFed(const Mle::RouteTlv &aRouteTlv, uint8_t aParentId)
+{
+    for (uint8_t routerId = 0, index = 0; routerId <= Mle::kMaxRouterId;
+         index += aRouteTlv.IsRouterIdSet(routerId) ? 1 : 0, routerId++)
+    {
+        Router *router;
+        uint8_t cost;
+        uint8_t nextHopId;
+
+        if (!aRouteTlv.IsRouterIdSet(routerId) || (routerId == aParentId))
+        {
+            continue;
+        }
+
+        router = FindRouterById(routerId);
+
+        if (router == nullptr)
+        {
+            continue;
+        }
+
+        cost      = aRouteTlv.GetRouteCost(index);
+        nextHopId = (cost == 0) ? Mle::kInvalidRouterId : aParentId;
+
+        if (router->SetNextHopAndCost(nextHopId, cost))
+        {
+            SignalTableChanged();
+        }
+    }
+}
+
+void RouterTable::FillRouteTlv(Mle::RouteTlv &aRouteTlv, const Neighbor *aNeighbor) const
+{
+    uint8_t          routerIdSequence = mRouterIdSequence;
+    Mle::RouterIdSet routerIdSet;
+    uint8_t          routerIndex;
+
+    mRouterIdMap.GetAsRouterIdSet(routerIdSet);
+
+    if ((aNeighbor != nullptr) && Mle::IsActiveRouter(aNeighbor->GetRloc16()))
+    {
+        // Sending a Link Accept message that may require truncation
+        // of Route64 TLV.
+
+        uint8_t routerCount = mRouters.GetLength();
+
+        if (routerCount > Mle::kLinkAcceptMaxRouters)
+        {
+            for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+            {
+                if (routerCount <= Mle::kLinkAcceptMaxRouters)
+                {
+                    break;
+                }
+
+                if ((routerId == Mle::RouterIdFromRloc16(Get<Mle::Mle>().GetRloc16())) ||
+                    (routerId == aNeighbor->GetRouterId()) || (routerId == Get<Mle::Mle>().GetLeaderId()))
+                {
+                    // Route64 TLV must contain this device and the
+                    // neighboring router to ensure that at least this
+                    // link can be established.
+                    continue;
+                }
+
+                if (routerIdSet.Contains(routerId))
+                {
+                    routerIdSet.Remove(routerId);
+                    routerCount--;
+                }
+            }
+
+            // Ensure that the neighbor will process the current
+            // Route64 TLV in a subsequent message exchange
+            routerIdSequence -= Mle::kLinkAcceptSequenceRollback;
+        }
+    }
+
+    aRouteTlv.SetRouterIdSequence(routerIdSequence);
+    aRouteTlv.SetRouterIdMask(routerIdSet);
+
+    routerIndex = 0;
+
+    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+    {
+        uint16_t routerRloc16;
+
+        if (!routerIdSet.Contains(routerId))
+        {
+            continue;
+        }
+
+        routerRloc16 = Mle::Rloc16FromRouterId(routerId);
+
+        if (routerRloc16 == Get<Mle::Mle>().GetRloc16())
+        {
+            aRouteTlv.SetRouteData(routerIndex, kLinkQuality0, kLinkQuality0, 1);
+        }
+        else
+        {
+            const Router *router = FindRouterById(routerId);
+            uint8_t       pathCost;
+
+            OT_ASSERT(router != nullptr);
+
+            pathCost = GetPathCost(routerRloc16);
+
+            if (pathCost >= Mle::kMaxRouteCost)
+            {
+                pathCost = 0;
+            }
+
+            aRouteTlv.SetRouteData(routerIndex, router->GetLinkQualityIn(), router->GetLinkQualityOut(), pathCost);
+        }
+
+        routerIndex++;
+    }
+
+    aRouteTlv.SetRouteDataLength(routerIndex);
+}
+
+void RouterTable::HandleTimeTick(void)
+{
+    mRouterIdMap.HandleTimeTick();
+
+    VerifyOrExit(Get<Mle::MleRouter>().IsLeader());
+
+    // Update router id sequence
+    if (GetLeaderAge() >= Mle::kRouterIdSequencePeriod)
+    {
+        mRouterIdSequence++;
+        mRouterIdSequenceLastUpdated = TimerMilli::GetNow();
+    }
+
+exit:
+    return;
 }
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
@@ -567,6 +843,91 @@
 }
 #endif
 
+void RouterTable::RouterIdMap::GetAsRouterIdSet(Mle::RouterIdSet &aRouterIdSet) const
+{
+    aRouterIdSet.Clear();
+
+    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+    {
+        if (IsAllocated(routerId))
+        {
+            aRouterIdSet.Add(routerId);
+        }
+    }
+}
+
+void RouterTable::RouterIdMap::HandleTimeTick(void)
+{
+    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+    {
+        // If Router ID is not allocated the `mIndexes` tracks the
+        // remaining reuse delay time in seconds.
+
+        if (!IsAllocated(routerId) && (mIndexes[routerId] > 0))
+        {
+            mIndexes[routerId]--;
+        }
+    }
+}
+
+void RouterTable::SignalTableChanged(void) { mChangedTask.Post(); }
+
+void RouterTable::HandleTableChanged(void)
+{
+#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
+    LogRouteTable();
+#endif
+
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
+    Get<Utils::HistoryTracker>().RecordRouterTableChange();
+#endif
+}
+
+#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
+void RouterTable::LogRouteTable(void) const
+{
+    static constexpr uint16_t kStringSize = 128;
+
+    LogInfo("Route table");
+
+    for (const Router &router : mRouters)
+    {
+        String<kStringSize> string;
+
+        string.Append("    %2d 0x%04x", router.GetRouterId(), router.GetRloc16());
+
+        if (router.GetRloc16() == Get<Mle::Mle>().GetRloc16())
+        {
+            string.Append(" - me");
+        }
+        else if (Get<Mle::Mle>().IsChild() && (router.GetRloc16() == Get<Mle::Mle>().GetParent().GetRloc16()))
+        {
+            string.Append(" - parent");
+        }
+        else
+        {
+            if (router.IsStateValid())
+            {
+                string.Append(" - nbr{lq[i/o]:%d/%d cost:%d}", router.GetLinkQualityIn(), router.GetLinkQualityOut(),
+                              GetLinkCost(router));
+            }
+
+            if (router.GetNextHop() != Mle::kInvalidRouterId)
+            {
+                string.Append(" - nexthop{%d cost:%d}", router.GetNextHop(), router.GetCost());
+            }
+        }
+
+        if (router.GetRouterId() == Get<Mle::Mle>().GetLeaderId())
+        {
+            string.Append(" - leader");
+        }
+
+        LogInfo("%s", string.AsCString());
+    }
+}
+#endif
+
 } // namespace ot
 
 #endif // OPENTHREAD_FTD
diff --git a/src/core/thread/router_table.hpp b/src/core/thread/router_table.hpp
index 7fa6549..e87ed0d 100644
--- a/src/core/thread/router_table.hpp
+++ b/src/core/thread/router_table.hpp
@@ -33,12 +33,16 @@
 
 #if OPENTHREAD_FTD
 
+#include "common/array.hpp"
 #include "common/const_cast.hpp"
 #include "common/encoding.hpp"
 #include "common/iterator_utils.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
+#include "common/serial_number.hpp"
+#include "common/tasklet.hpp"
 #include "mac/mac_types.hpp"
+#include "thread/mle_tlvs.hpp"
 #include "thread/mle_types.hpp"
 #include "thread/thread_tlvs.hpp"
 #include "thread/topology.hpp"
@@ -48,42 +52,9 @@
 class RouterTable : public InstanceLocator, private NonCopyable
 {
     friend class NeighborTable;
-    class IteratorBuilder;
 
 public:
     /**
-     * This class represents an iterator for iterating through entries in the router table.
-     *
-     */
-    class Iterator : public InstanceLocator, public ItemPtrIterator<Router, Iterator>
-    {
-        friend class ItemPtrIterator<Router, Iterator>;
-        friend class IteratorBuilder;
-
-    public:
-        /**
-         * This constructor initializes an `Iterator` instance to start from beginning of the router table.
-         *
-         * @param[in] aInstance  A reference to the OpenThread instance.
-         *
-         */
-        explicit Iterator(Instance &aInstance);
-
-    private:
-        enum IteratorType : uint8_t
-        {
-            kEndIterator,
-        };
-
-        Iterator(Instance &aInstance, IteratorType)
-            : InstanceLocator(aInstance)
-        {
-        }
-
-        void Advance(void);
-    };
-
-    /**
      * Constructor.
      *
      * @param[in]  aInstance  A reference to the OpenThread instance.
@@ -104,29 +75,31 @@
     void ClearNeighbors(void);
 
     /**
-     * This method allocates a router with a random router id.
+     * This method allocates a router with a random Router ID.
      *
-     * @returns A pointer to the allocated router or `nullptr` if a router ID is not available.
+     * @returns A pointer to the allocated router or `nullptr` if a Router ID is not available.
      *
      */
     Router *Allocate(void);
 
     /**
-     * This method allocates a router with a specified router id.
+     * This method allocates a router with a specified Router ID.
      *
-     * @returns A pointer to the allocated router or `nullptr` if the router id could not be allocated.
+     * @param[in] aRouterId   The Router ID to try to allocate.
+     *
+     * @returns A pointer to the allocated router or `nullptr` if the ID @p aRouterId could not be allocated.
      *
      */
     Router *Allocate(uint8_t aRouterId);
 
     /**
-     * This method releases a router id.
+     * This method releases a Router ID.
      *
-     * @param[in]  aRouterId  The router id.
+     * @param[in]  aRouterId  The Router ID.
      *
-     * @retval kErrorNone          Successfully released the router id.
+     * @retval kErrorNone          Successfully released the Router ID @p aRouterId.
      * @retval kErrorInvalidState  The device is not currently operating as a leader.
-     * @retval kErrorNotFound      The router id is not currently allocated.
+     * @retval kErrorNotFound      The Router ID @p aRouterId is not currently allocated.
      *
      */
     Error Release(uint8_t aRouterId);
@@ -145,15 +118,7 @@
      * @returns The number of active routers in the Thread network.
      *
      */
-    uint8_t GetActiveRouterCount(void) const { return mActiveRouterCount; }
-
-    /**
-     * This method returns the number of active links with neighboring routers.
-     *
-     * @returns The number of active links with neighboring routers.
-     *
-     */
-    uint8_t GetActiveLinkCount(void) const;
+    uint8_t GetActiveRouterCount(void) const { return mRouters.GetLength(); }
 
     /**
      * This method returns the leader in the Thread network.
@@ -161,12 +126,20 @@
      * @returns A pointer to the Leader in the Thread network.
      *
      */
-    Router *GetLeader(void);
+    Router *GetLeader(void) { return AsNonConst(AsConst(this)->GetLeader()); }
 
     /**
-     * This method returns the time in seconds since the last Router ID Sequence update.
+     * This method returns the leader in the Thread network.
      *
-     * @returns The time in seconds since the last Router ID Sequence update.
+     * @returns A pointer to the Leader in the Thread network.
+     *
+     */
+    const Router *GetLeader(void) const;
+
+    /**
+     * This method returns the leader's age in seconds, i.e., seconds since the last Router ID Sequence update.
+     *
+     * @returns The leader's age.
      *
      */
     uint32_t GetLeaderAge(void) const;
@@ -174,87 +147,144 @@
     /**
      * This method returns the link cost for a neighboring router.
      *
-     * @param[in]  aRouter  A reference to the router.
+     * @param[in]  aRouter   A router.
      *
-     * @returns The link cost.
+     * @returns The link cost to @p aRouter.
      *
      */
-    uint8_t GetLinkCost(Router &aRouter);
+    uint8_t GetLinkCost(const Router &aRouter) const;
 
     /**
-     * This method returns the neighbor for a given RLOC16.
+     * This method returns the link cost to the given Router.
      *
-     * @param[in]  aRloc16  The RLOC16 value.
+     * @param[in]  aRouterId  The Router ID.
+     *
+     * @returns The link cost to the Router.
+     *
+     */
+    uint8_t GetLinkCost(uint8_t aRouterId) const;
+
+    /**
+     * This method returns the minimum mesh path cost to the given RLOC16
+     *
+     * @param[in]  aDestRloc16  The RLOC16 of destination
+     *
+     * @returns The minimum mesh path cost to @p aDestRloc16 (via direct link or forwarding).
+     *
+     */
+    uint8_t GetPathCost(uint16_t aDestRloc16) const;
+
+    /**
+     * This method returns the mesh path cost to leader.
+     *
+     * @returns The path cost to leader.
+     *
+     */
+    uint8_t GetPathCostToLeader(void) const;
+
+    /**
+     * This method determines the next hop towards an RLOC16 destination.
+     *
+     * @param[in]  aDestRloc16  The RLOC16 of the destination.
+     *
+     * @returns A RLOC16 of the next hop if a route is known, `Mle::kInvalidRloc16` otherwise.
+     *
+     */
+    uint16_t GetNextHop(uint16_t aDestRloc16) const;
+
+    /**
+     * This method determines the next hop and the path cost towards an RLOC16 destination.
+     *
+     * @param[in]  aDestRloc16      The RLOC16 of the destination.
+     * @param[out] aNextHopRloc16   A reference to return the RLOC16 of next hop if known, or `Mle::kInvalidRloc16`.
+     * @param[out] aPathCost        A reference to return the path cost.
+     *
+     */
+    void GetNextHopAndPathCost(uint16_t aDestRloc16, uint16_t &aNextHopRloc16, uint8_t &aPathCost) const;
+
+    /**
+     * This method finds the router for a given Router ID.
+     *
+     * @param[in]  aRouterId  The Router ID to search for.
      *
      * @returns A pointer to the router or `nullptr` if the router could not be found.
      *
      */
-    Router *GetNeighbor(uint16_t aRloc16);
+    Router *FindRouterById(uint8_t aRouterId) { return AsNonConst(AsConst(this)->FindRouterById(aRouterId)); }
 
     /**
-     * This method returns the neighbor for a given IEEE Extended Address.
+     * This method finds the router for a given Router ID.
      *
-     * @param[in]  aExtAddress  A reference to the IEEE Extended Address.
+     * @param[in]  aRouterId  The Router ID to search for.
      *
      * @returns A pointer to the router or `nullptr` if the router could not be found.
      *
      */
-    Router *GetNeighbor(const Mac::ExtAddress &aExtAddress);
+    const Router *FindRouterById(uint8_t aRouterId) const;
 
     /**
-     * This method returns the neighbor for a given MAC address.
+     * This method finds the router for a given RLOC16.
      *
-     * @param[in]  aMacAddress  A MAC address
+     * @param[in]  aRloc16  The RLOC16 to search for.
      *
      * @returns A pointer to the router or `nullptr` if the router could not be found.
      *
      */
-    Router *GetNeighbor(const Mac::Address &aMacAddress);
+    Router *FindRouterByRloc16(uint16_t aRloc16) { return AsNonConst(AsConst(this)->FindRouterByRloc16(aRloc16)); }
 
     /**
-     * This method returns the router for a given router id.
+     * This method finds the router for a given RLOC16.
      *
-     * @param[in]  aRouterId  The router id.
+     * @param[in]  aRloc16  The RLOC16 to search for.
      *
      * @returns A pointer to the router or `nullptr` if the router could not be found.
      *
      */
-    Router *GetRouter(uint8_t aRouterId) { return AsNonConst(AsConst(this)->GetRouter(aRouterId)); }
+    const Router *FindRouterByRloc16(uint16_t aRloc16) const;
 
     /**
-     * This method returns the router for a given router id.
+     * This method finds the router that is the next hop of a given router.
      *
-     * @param[in]  aRouterId  The router id.
+     * @param[in]  aRouter  The router to find next hop of.
      *
      * @returns A pointer to the router or `nullptr` if the router could not be found.
      *
      */
-    const Router *GetRouter(uint8_t aRouterId) const;
+    Router *FindNextHopOf(const Router &aRouter) { return AsNonConst(AsConst(this)->FindNextHopOf(aRouter)); }
 
     /**
-     * This method returns the router for a given IEEE Extended Address.
+     * This method finds the router that is the next hop of a given router.
      *
-     * @param[in]  aExtAddress  A reference to the IEEE Extended Address.
+     * @param[in]  aRouter  The router to find next hop of.
      *
      * @returns A pointer to the router or `nullptr` if the router could not be found.
      *
      */
-    Router *GetRouter(const Mac::ExtAddress &aExtAddress);
+    const Router *FindNextHopOf(const Router &aRouter) const;
 
     /**
-     * This method returns if the router table contains a given `Neighbor` instance.
+     * This method find the router for a given MAC Extended Address.
+     *
+     * @param[in]  aExtAddress  A reference to the MAC Extended Address.
+     *
+     * @returns A pointer to the router or `nullptr` if the router could not be found.
+     *
+     */
+    Router *FindRouter(const Mac::ExtAddress &aExtAddress);
+
+    /**
+     * This method indicates whether the router table contains a given `Neighbor` instance.
      *
      * @param[in]  aNeighbor  A reference to a `Neighbor`.
      *
      * @retval TRUE  if @p aNeighbor is a `Router` in the router table.
      * @retval FALSE if @p aNeighbor is not a `Router` in the router table
-     *               (i.e. mParent, mParentCandidate, a `Child` of the child table).
+     *               (i.e. it can be the parent or parent candidate, or a `Child` of the child table).
      *
      */
     bool Contains(const Neighbor &aNeighbor) const
     {
-        return mRouters <= &static_cast<const Router &>(aNeighbor) &&
-               &static_cast<const Router &>(aNeighbor) < mRouters + Mle::kMaxRouters;
+        return mRouters.IsInArrayBuffer(&static_cast<const Router &>(aNeighbor));
     }
 
     /**
@@ -287,6 +317,18 @@
     TimeMilli GetRouterIdSequenceLastUpdated(void) const { return mRouterIdSequenceLastUpdated; }
 
     /**
+     * This method determines whether the Router ID Sequence in a received Route TLV is more recent than the current
+     * Router ID Sequence being used by `RouterTable`.
+     *
+     * @param[in] aRouteTlv   The Route TLV to compare.
+     *
+     * @retval TRUE    The Router ID Sequence in @p aRouteTlv is more recent.
+     * @retval FALSE   The Router ID Sequence in @p aRouteTlv is not more recent.
+     *
+     */
+    bool IsRouteTlvIdSequenceMoreRecent(const Mle::RouteTlv &aRouteTlv) const;
+
+    /**
      * This method returns the number of neighbor links.
      *
      * @returns The number of neighbor links.
@@ -295,30 +337,65 @@
     uint8_t GetNeighborCount(void) const;
 
     /**
-     * This method indicates whether or not @p aRouterId is allocated.
+     * This method indicates whether or not a Router ID is allocated.
      *
-     * @retval TRUE if @p aRouterId is allocated.
+     * @param[in] aRouterId  The Router ID.
+     *
+     * @retval TRUE  if @p aRouterId is allocated.
      * @retval FALSE if @p aRouterId is not allocated.
      *
      */
-    bool IsAllocated(uint8_t aRouterId) const;
+    bool IsAllocated(uint8_t aRouterId) const { return mRouterIdMap.IsAllocated(aRouterId); }
 
     /**
-     * This method updates the Router ID allocation.
+     * This method updates the Router ID allocation set.
      *
-     * @param[in]  aRouterIdSequence  The Router Id Sequence.
-     * @param[in]  aRouterIdSet       A reference to the Router Id Set.
+     * @param[in]  aRouterIdSequence  The Router ID Sequence.
+     * @param[in]  aRouterIdSet       The Router ID Set.
      *
      */
     void UpdateRouterIdSet(uint8_t aRouterIdSequence, const Mle::RouterIdSet &aRouterIdSet);
 
     /**
+     * This method updates the routes based on a received `RouteTlv` from a neighboring router.
+     *
+     * @param[in]  aRouteTlv    The received `RouteTlv`
+     * @param[in]  aNeighborId  The router ID of neighboring router from which @p aRouteTlv is received.
+     *
+     */
+    void UpdateRoutes(const Mle::RouteTlv &aRouteTlv, uint8_t aNeighborId);
+
+    /**
+     * This method updates the routes on an FED based on a received `RouteTlv` from the parent.
+     *
+     * This method MUST be called when device is an FED child and @p aRouteTlv is received from its current parent.
+     *
+     * @param[in]  aRouteTlv    The received `RouteTlv` from parent.
+     * @param[in]  aParentId    The Router ID of parent.
+     *
+     */
+    void UpdateRoutesOnFed(const Mle::RouteTlv &aRouteTlv, uint8_t aParentId);
+
+    /**
      * This method gets the allocated Router ID set.
      *
      * @returns The allocated Router ID set.
      *
      */
-    const Mle::RouterIdSet &GetRouterIdSet(void) const { return mAllocatedRouterIds; }
+    void GetRouterIdSet(Mle::RouterIdSet &aRouterIdSet) const { return mRouterIdMap.GetAsRouterIdSet(aRouterIdSet); }
+
+    /**
+     * This method fills a Route TLV.
+     *
+     * When @p aNeighbor is not `nullptr`, we limit the number of router entries to `Mle::kLinkAcceptMaxRouters` when
+     * populating `aRouteTlv`, so that the TLV can be appended in a Link Accept message. In this case, we ensure to
+     * include router entries associated with @p aNeighbor, leader, and this device itself.
+     *
+     * @param[out] aRouteTlv    A Route TLV to be filled.
+     * @param[in]  aNeighbor    A pointer to the receiver (in case TLV is for a Link Accept message).
+     *
+     */
+    void FillRouteTlv(Mle::RouteTlv &aRouteTlv, const Neighbor *aNeighbor = nullptr) const;
 
     /**
      * This method updates the router table and must be called with a one second period.
@@ -326,55 +403,96 @@
      */
     void HandleTimeTick(void);
 
+#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     /**
-     * This method enables range-based `for` loop iteration over all Router entries in the Router table.
+     * This method gets the range of Router IDs.
      *
-     * This method should be used as follows:
+     * All the Router IDs in the range `[aMinRouterId, aMaxRouterId]` are allowed. This is intended for testing only.
      *
-     *     for (Router &router : Get<RouterTable>().Iterate()) { ... }
-     *
-     * @returns An `IteratorBuilder` instance.
+     * @param[out]  aMinRouterId   Reference to return the minimum Router ID.
+     * @param[out]  aMaxRouterId   Reference to return the maximum Router ID.
      *
      */
-    IteratorBuilder Iterate(void) { return IteratorBuilder(GetInstance()); }
-
-#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     void GetRouterIdRange(uint8_t &aMinRouterId, uint8_t &aMaxRouterId) const;
 
+    /**
+     * This method sets the range of Router IDs.
+     *
+     * All the Router IDs in the range `[aMinRouterId, aMaxRouterId]` are allowed. This is intended for testing only.
+     *
+     * @param[in]  aMinRouterId   The minimum Router ID.
+     * @param[in]  aMaxRouterId   The maximum Router ID.
+     *
+     * @retval kErrorNone          Successfully set the Router ID range.
+     * @retval kErrorInvalidArgs   The given range is not valid.
+     *
+     */
     Error SetRouterIdRange(uint8_t aMinRouterId, uint8_t aMaxRouterId);
 #endif
 
+    // The following methods are intended to support range-based `for`
+    // loop iteration over the router and should not be used
+    // directly.
+
+    Router       *begin(void) { return mRouters.begin(); }
+    Router       *end(void) { return mRouters.end(); }
+    const Router *begin(void) const { return mRouters.begin(); }
+    const Router *end(void) const { return mRouters.end(); }
+
 private:
-    class IteratorBuilder : public InstanceLocator
-    {
-    public:
-        explicit IteratorBuilder(Instance &aInstance)
-            : InstanceLocator(aInstance)
-        {
-        }
-
-        Iterator begin(void) { return Iterator(GetInstance()); }
-        Iterator end(void) { return Iterator(GetInstance(), Iterator::kEndIterator); }
-    };
-
-    void          UpdateAllocation(void);
-    const Router *GetFirstEntry(void) const;
-    const Router *GetNextEntry(const Router *aRouter) const;
-    Router *      GetFirstEntry(void) { return AsNonConst(AsConst(this)->GetFirstEntry()); }
-    Router *      GetNextEntry(Router *aRouter) { return AsNonConst(AsConst(this)->GetNextEntry(aRouter)); }
-
+    Router       *AddRouter(uint8_t aRouterId);
+    void          RemoveRouter(Router &aRouter);
+    Router       *FindNeighbor(uint16_t aRloc16);
+    Router       *FindNeighbor(const Mac::ExtAddress &aExtAddress);
+    Router       *FindNeighbor(const Mac::Address &aMacAddress);
     const Router *FindRouter(const Router::AddressMatcher &aMatcher) const;
-    Router *      FindRouter(const Router::AddressMatcher &aMatcher)
+    Router       *FindRouter(const Router::AddressMatcher &aMatcher)
     {
         return AsNonConst(AsConst(this)->FindRouter(aMatcher));
     }
 
-    Router           mRouters[Mle::kMaxRouters];
-    Mle::RouterIdSet mAllocatedRouterIds;
-    uint8_t          mRouterIdReuseDelay[Mle::kMaxRouterId + 1];
-    TimeMilli        mRouterIdSequenceLastUpdated;
-    uint8_t          mRouterIdSequence;
-    uint8_t          mActiveRouterCount;
+    void SignalTableChanged(void);
+    void HandleTableChanged(void);
+    void LogRouteTable(void) const;
+
+    class RouterIdMap
+    {
+    public:
+        // The `RouterIdMap` tracks which Router IDs are allocated.
+        // For allocated IDs, tracks the index of the `Router` entry
+        // in `mRouters` array. For unallocated IDs, tracks the
+        // remaining reuse delay time (in seconds).
+
+        RouterIdMap(void) { Clear(); }
+        void    Clear(void) { memset(mIndexes, 0, sizeof(mIndexes)); }
+        bool    IsAllocated(uint8_t aRouterId) const { return (mIndexes[aRouterId] & kAllocatedFlag); }
+        uint8_t GetIndex(uint8_t aRouterId) const { return (mIndexes[aRouterId] & kIndexMask); }
+        void    SetIndex(uint8_t aRouterId, uint8_t aIndex) { mIndexes[aRouterId] = kAllocatedFlag | aIndex; }
+        bool    CanAllocate(uint8_t aRouterId) const { return (mIndexes[aRouterId] == 0); }
+        void    Release(uint8_t aRouterId) { mIndexes[aRouterId] = Mle::kRouterIdReuseDelay; }
+        void    GetAsRouterIdSet(Mle::RouterIdSet &aRouterIdSet) const;
+        void    HandleTimeTick(void);
+
+    private:
+        // The high bit in `mIndexes[aRouterId]` indicates whether or
+        // not the router ID is allocated. The lower 7 bits give either
+        // the index in `mRouter` array or remaining reuse delay time.
+
+        static constexpr uint8_t kAllocatedFlag = 1 << 7;
+        static constexpr uint8_t kIndexMask     = 0x7f;
+
+        static_assert(Mle::kRouterIdReuseDelay <= kIndexMask, "Mle::kRouterIdReuseDelay does not fit in 7 bits");
+
+        uint8_t mIndexes[Mle::kMaxRouterId + 1];
+    };
+
+    using ChangedTask = TaskletIn<RouterTable, &RouterTable::HandleTableChanged>;
+
+    Array<Router, Mle::kMaxRouters> mRouters;
+    ChangedTask                     mChangedTask;
+    RouterIdMap                     mRouterIdMap;
+    TimeMilli                       mRouterIdSequenceLastUpdated;
+    uint8_t                         mRouterIdSequence;
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     uint8_t mMinRouterId;
     uint8_t mMaxRouterId;
diff --git a/src/core/thread/thread_netif.cpp b/src/core/thread/thread_netif.cpp
index 3866d0d..e01d192 100644
--- a/src/core/thread/thread_netif.cpp
+++ b/src/core/thread/thread_netif.cpp
@@ -98,7 +98,7 @@
     Get<Dns::ServiceDiscovery::Server>().Stop();
 #endif
 #if OPENTHREAD_CONFIG_DTLS_ENABLE
-    Get<Coap::CoapSecure>().Stop();
+    Get<Tmf::SecureAgent>().Stop();
 #endif
     IgnoreError(Get<Tmf::Agent>().Stop());
     IgnoreError(Get<Mle::MleRouter>().Disable());
@@ -118,30 +118,4 @@
     return;
 }
 
-Error ThreadNetif::SendMessage(Message &aMessage)
-{
-    return Get<MeshForwarder>().SendMessage(aMessage);
-}
-
-Error ThreadNetif::RouteLookup(const Ip6::Address &aSource, const Ip6::Address &aDestination, uint8_t *aPrefixMatch)
-{
-    Error    error;
-    uint16_t rloc;
-
-    SuccessOrExit(error = Get<NetworkData::Leader>().RouteLookup(aSource, aDestination, aPrefixMatch, &rloc));
-
-    if (rloc == Get<Mle::MleRouter>().GetRloc16())
-    {
-        error = kErrorNoRoute;
-    }
-
-exit:
-    return error;
-}
-
-bool ThreadNetif::IsOnMesh(const Ip6::Address &aAddress) const
-{
-    return Get<NetworkData::Leader>().IsOnMesh(aAddress);
-}
-
 } // namespace ot
diff --git a/src/core/thread/thread_netif.hpp b/src/core/thread/thread_netif.hpp
index e06e6b6..af776be 100644
--- a/src/core/thread/thread_netif.hpp
+++ b/src/core/thread/thread_netif.hpp
@@ -82,40 +82,6 @@
      */
     bool IsUp(void) const { return mIsUp; }
 
-    /**
-     * This method submits a message to the network interface.
-     *
-     * @param[in]  aMessage  A reference to the message.
-     *
-     * @retval kErrorNone  Successfully submitted the message to the interface.
-     *
-     */
-    Error SendMessage(Message &aMessage);
-
-    /**
-     * This method performs a route lookup.
-     *
-     * @param[in]   aSource       A reference to the IPv6 source address.
-     * @param[in]   aDestination  A reference to the IPv6 destination address.
-     * @param[out]  aPrefixMatch  A pointer where the number of prefix match bits for the chosen route is stored.
-     *
-     * @retval kErrorNone      Successfully found a route.
-     * @retval kErrorNoRoute   Could not find a valid route.
-     *
-     */
-    Error RouteLookup(const Ip6::Address &aSource, const Ip6::Address &aDestination, uint8_t *aPrefixMatch);
-
-    /**
-     * This method indicates whether @p aAddress matches an on-mesh prefix.
-     *
-     * @param[in]  aAddress  The IPv6 address.
-     *
-     * @retval TRUE   If @p aAddress matches an on-mesh prefix.
-     * @retval FALSE  If @p aAddress does not match an on-mesh prefix.
-     *
-     */
-    bool IsOnMesh(const Ip6::Address &aAddress) const;
-
 private:
     bool mIsUp;
 };
diff --git a/src/core/thread/thread_tlvs.hpp b/src/core/thread/thread_tlvs.hpp
index f092c5f..4527aac 100644
--- a/src/core/thread/thread_tlvs.hpp
+++ b/src/core/thread/thread_tlvs.hpp
@@ -39,6 +39,7 @@
 #include "common/encoding.hpp"
 #include "common/message.hpp"
 #include "common/tlvs.hpp"
+#include "meshcop/network_name.hpp"
 #include "net/ip6_address.hpp"
 #include "thread/mle.hpp"
 #include "thread/mle_types.hpp"
@@ -136,7 +137,7 @@
  * This class defines Network Name TLV constants and types.
  *
  */
-typedef TlvInfo<ThreadTlv::kNetworkName> ThreadNetworkNameTlv;
+typedef StringTlvInfo<ThreadTlv::kNetworkName, MeshCoP::NetworkName::kMaxSize> ThreadNetworkNameTlv;
 
 /**
  * This class defines Commissioner Session ID TLV constants and types.
@@ -249,6 +250,14 @@
     const Mle::RouterIdSet &GetAssignedRouterIdMask(void) const { return mAssignedRouterIdMask; }
 
     /**
+     * This method gets the Assigned Router ID Mask.
+     *
+     * @returns The Assigned Router ID Mask.
+     *
+     */
+    Mle::RouterIdSet &GetAssignedRouterIdMask(void) { return mAssignedRouterIdMask; }
+
+    /**
      * This method sets the Assigned Router ID Mask.
      *
      * @param[in]  aRouterIdSet A reference to the Assigned Router ID Mask.
diff --git a/src/core/thread/time_sync_service.cpp b/src/core/thread/time_sync_service.cpp
index e938f38..138c1aa 100644
--- a/src/core/thread/time_sync_service.cpp
+++ b/src/core/thread/time_sync_service.cpp
@@ -62,15 +62,13 @@
 #endif
     , mLastTimeSyncReceived(0)
     , mNetworkTimeOffset(0)
-    , mTimeSyncCallback(nullptr)
-    , mTimeSyncCallbackContext(nullptr)
-    , mTimer(aInstance, HandleTimeout)
-    , mCurrentStatus(OT_NETWORK_TIME_UNSYNCHRONIZED)
+    , mTimer(aInstance)
+    , mCurrentStatus(kUnsynchronized)
 {
     CheckAndHandleChanges(false);
 }
 
-otNetworkTimeStatus TimeSync::GetTime(uint64_t &aNetworkTime) const
+TimeSync::Status TimeSync::GetTime(uint64_t &aNetworkTime) const
 {
     aNetworkTime = static_cast<uint64_t>(static_cast<int64_t>(otPlatTimeGet()) + mNetworkTimeOffset);
 
@@ -140,13 +138,7 @@
     }
 }
 
-void TimeSync::NotifyTimeSyncCallback(void)
-{
-    if (mTimeSyncCallback != nullptr)
-    {
-        mTimeSyncCallback(mTimeSyncCallbackContext);
-    }
-}
+void TimeSync::NotifyTimeSyncCallback(void) { mTimeSyncCallback.InvokeIfSet(); }
 
 #if OPENTHREAD_FTD
 void TimeSync::ProcessTimeSync(void)
@@ -189,7 +181,7 @@
         mTimeSyncSeq      = OT_TIME_SYNC_INVALID_SEQ;
         mTimeSyncRequired = false;
 
-        // Network time status will become OT_NETWORK_TIME_UNSYNCHRONIZED because no network time has yet been received
+        // Network time status will become kUnsychronized because no network time has yet been received
         // on the new partition.
         mLastTimeSyncReceived.SetValue(0);
 
@@ -204,21 +196,13 @@
     }
 }
 
-void TimeSync::HandleTimeout(void)
-{
-    CheckAndHandleChanges(false);
-}
-
-void TimeSync::HandleTimeout(Timer &aTimer)
-{
-    aTimer.Get<TimeSync>().HandleTimeout();
-}
+void TimeSync::HandleTimeout(void) { CheckAndHandleChanges(false); }
 
 void TimeSync::CheckAndHandleChanges(bool aTimeUpdated)
 {
-    otNetworkTimeStatus networkTimeStatus       = OT_NETWORK_TIME_SYNCHRONIZED;
-    const uint32_t      resyncNeededThresholdMs = 2 * Time::SecToMsec(mTimeSyncPeriod);
-    const uint32_t      timeSyncLastSyncMs      = TimerMilli::GetNow() - mLastTimeSyncReceived;
+    Status         networkTimeStatus       = kSynchronized;
+    const uint32_t resyncNeededThresholdMs = 2 * Time::SecToMsec(mTimeSyncPeriod);
+    const uint32_t timeSyncLastSyncMs      = TimerMilli::GetNow() - mLastTimeSyncReceived;
 
     mTimer.Stop();
 
@@ -226,7 +210,7 @@
     {
     case Mle::kRoleDisabled:
     case Mle::kRoleDetached:
-        networkTimeStatus = OT_NETWORK_TIME_UNSYNCHRONIZED;
+        networkTimeStatus = kUnsynchronized;
         LogInfo("Time sync status UNSYNCHRONIZED as role:DISABLED/DETACHED");
         break;
 
@@ -235,15 +219,15 @@
         if (mLastTimeSyncReceived.GetValue() == 0)
         {
             // Haven't yet received any time sync
-            networkTimeStatus = OT_NETWORK_TIME_UNSYNCHRONIZED;
+            networkTimeStatus = kUnsynchronized;
             LogInfo("Time sync status UNSYNCHRONIZED as mLastTimeSyncReceived:0");
         }
         else if (timeSyncLastSyncMs > resyncNeededThresholdMs)
         {
             // The device hasn’t received time sync for more than two periods time.
-            networkTimeStatus = OT_NETWORK_TIME_RESYNC_NEEDED;
-            LogInfo("Time sync status RESYNC_NEEDED as timeSyncLastSyncMs:%u > resyncNeededThresholdMs:%u",
-                    timeSyncLastSyncMs, resyncNeededThresholdMs);
+            networkTimeStatus = kResyncNeeded;
+            LogInfo("Time sync status RESYNC_NEEDED as timeSyncLastSyncMs:%lu > resyncNeededThresholdMs:%lu",
+                    ToUlong(timeSyncLastSyncMs), ToUlong(resyncNeededThresholdMs));
         }
         else
         {
diff --git a/src/core/thread/time_sync_service.hpp b/src/core/thread/time_sync_service.hpp
index 4f2e144..d0ad1ac 100644
--- a/src/core/thread/time_sync_service.hpp
+++ b/src/core/thread/time_sync_service.hpp
@@ -40,6 +40,8 @@
 
 #include <openthread/network_time.h>
 
+#include "common/as_core_type.hpp"
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/message.hpp"
 #include "common/non_copyable.hpp"
@@ -58,6 +60,17 @@
 
 public:
     /**
+     * This enumeration represents Network Time Status
+     *
+     */
+    enum Status : int8_t
+    {
+        kUnsynchronized = OT_NETWORK_TIME_UNSYNCHRONIZED, ///< The device hasn't attached to a network.
+        kResyncNeeded   = OT_NETWORK_TIME_RESYNC_NEEDED,  ///< The device hasn’t received time sync for 2 periods.
+        kSynchronized   = OT_NETWORK_TIME_SYNCHRONIZED,   ///< The device network time is synchronized.
+    };
+
+    /**
      * This constructor initializes the object.
      *
      */
@@ -71,7 +84,7 @@
      * @returns The time synchronization status.
      *
      */
-    otNetworkTimeStatus GetTime(uint64_t &aNetworkTime) const;
+    Status GetTime(uint64_t &aNetworkTime) const;
 
     /**
      * Handle the message which includes time synchronization information.
@@ -151,8 +164,7 @@
      */
     void SetTimeSyncCallback(otNetworkTimeSyncCallbackFn aCallback, void *aCallbackContext)
     {
-        mTimeSyncCallback        = aCallback;
-        mTimeSyncCallbackContext = aCallbackContext;
+        mTimeSyncCallback.Set(aCallback, aCallbackContext);
     }
 
     /**
@@ -171,14 +183,6 @@
     void HandleNotifierEvents(Events aEvents);
 
     /**
-     * Callback to be called when timer expires.
-     *
-     * @param[in] aTimer The corresponding timer.
-     *
-     */
-    static void HandleTimeout(Timer &aTimer);
-
-    /**
      * Check and handle any status change, and notify observers if applicable.
      *
      * @param[in] aNotifyTimeUpdated True to denote that observers should be notified due to a time change, false
@@ -199,6 +203,8 @@
      */
     void NotifyTimeSyncCallback(void);
 
+    using SyncTimer = TimerMilliIn<TimeSync, &TimeSync::HandleTimeout>;
+
     bool     mTimeSyncRequired; ///< Indicate whether or not a time synchronization message is required.
     uint8_t  mTimeSyncSeq;      ///< The time synchronization sequence.
     uint16_t mTimeSyncPeriod;   ///< The time synchronization period.
@@ -208,13 +214,14 @@
 #endif
     TimeMilli mLastTimeSyncReceived; ///< The time when the last time synchronization message was received.
     int64_t   mNetworkTimeOffset;    ///< The time offset to the Thread Network time
-    otNetworkTimeSyncCallbackFn
-                        mTimeSyncCallback; ///< The callback to be called when time sync is handled or status updated.
-    void *              mTimeSyncCallbackContext; ///< The context to be passed to callback.
-    TimerMilli          mTimer;                   ///< Timer for checking if a resync is required.
-    otNetworkTimeStatus mCurrentStatus;           ///< Current network time status.
+
+    Callback<otNetworkTimeSyncCallbackFn> mTimeSyncCallback; ///< Callback when time sync is handled or status updated.
+    SyncTimer                             mTimer;            ///< Timer for checking if a resync is required.
+    Status                                mCurrentStatus;    ///< Current network time status.
 };
 
+DefineMapEnum(otNetworkTimeStatus, TimeSync::Status);
+
 /**
  * @}
  */
diff --git a/src/core/thread/tmf.cpp b/src/core/thread/tmf.cpp
index 24a4ae9..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 {
@@ -41,10 +42,7 @@
 //----------------------------------------------------------------------------------------------------------------------
 // MessageInfo
 
-void MessageInfo::SetSockAddrToRloc(void)
-{
-    SetSockAddr(Get<Mle::MleRouter>().GetMeshLocal16());
-}
+void MessageInfo::SetSockAddrToRloc(void) { SetSockAddr(Get<Mle::MleRouter>().GetMeshLocal16()); }
 
 Error MessageInfo::SetSockAddrToRlocPeerAddrToLeaderAloc(void)
 {
@@ -80,9 +78,118 @@
 //----------------------------------------------------------------------------------------------------------------------
 // Agent
 
-Error Agent::Start(void)
+Agent::Agent(Instance &aInstance)
+    : Coap::Coap(aInstance)
 {
-    return Coap::Start(kUdpPort, OT_NETIF_THREAD);
+    SetInterceptor(&Filter, this);
+    SetResourceHandler(&HandleResource);
+}
+
+Error Agent::Start(void) { return Coap::Start(kUdpPort, Ip6::kNetifThread); }
+
+template <> void Agent::HandleTmf<kUriRelayRx>(Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    OT_UNUSED_VARIABLE(aMessage);
+    OT_UNUSED_VARIABLE(aMessageInfo);
+
+#if (OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE)
+    Get<MeshCoP::Commissioner>().HandleTmf<kUriRelayRx>(aMessage, aMessageInfo);
+#endif
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
+    Get<MeshCoP::BorderAgent>().HandleTmf<kUriRelayRx>(aMessage, aMessageInfo);
+#endif
+}
+
+bool Agent::HandleResource(CoapBase               &aCoapBase,
+                           const char             *aUriPath,
+                           Message                &aMessage,
+                           const Ip6::MessageInfo &aMessageInfo)
+{
+    return static_cast<Agent &>(aCoapBase).HandleResource(aUriPath, aMessage, aMessageInfo);
+}
+
+bool Agent::HandleResource(const char *aUriPath, Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    bool didHandle = true;
+    Uri  uri       = UriFromPath(aUriPath);
+
+#define Case(kUri, Type)                                     \
+    case kUri:                                               \
+        Get<Type>().HandleTmf<kUri>(aMessage, aMessageInfo); \
+        break
+
+    switch (uri)
+    {
+        Case(kUriAddressError, AddressResolver);
+        Case(kUriEnergyScan, EnergyScanServer);
+        Case(kUriActiveGet, MeshCoP::ActiveDatasetManager);
+        Case(kUriPendingGet, MeshCoP::PendingDatasetManager);
+        Case(kUriPanIdQuery, PanIdQueryServer);
+
+#if OPENTHREAD_FTD
+        Case(kUriAddressQuery, AddressResolver);
+        Case(kUriAddressNotify, AddressResolver);
+        Case(kUriAddressSolicit, Mle::MleRouter);
+        Case(kUriAddressRelease, Mle::MleRouter);
+        Case(kUriActiveSet, MeshCoP::ActiveDatasetManager);
+        Case(kUriPendingSet, MeshCoP::PendingDatasetManager);
+        Case(kUriLeaderPetition, MeshCoP::Leader);
+        Case(kUriLeaderKeepAlive, MeshCoP::Leader);
+        Case(kUriServerData, NetworkData::Leader);
+        Case(kUriCommissionerGet, NetworkData::Leader);
+        Case(kUriCommissionerSet, NetworkData::Leader);
+        Case(kUriAnnounceBegin, AnnounceBeginServer);
+        Case(kUriRelayTx, MeshCoP::JoinerRouter);
+#endif
+
+#if OPENTHREAD_CONFIG_JOINER_ENABLE
+        Case(kUriJoinerEntrust, MeshCoP::Joiner);
+#endif
+
+#if OPENTHREAD_CONFIG_COMMISSIONER_ENABLE && OPENTHREAD_FTD
+        Case(kUriPanIdConflict, PanIdQueryClient);
+        Case(kUriEnergyReport, EnergyScanClient);
+        Case(kUriDatasetChanged, MeshCoP::Commissioner);
+        // kUriRelayRx is handled below
+#endif
+
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE || (OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE)
+        Case(kUriRelayRx, Agent);
+#endif
+
+#if OPENTHREAD_CONFIG_DUA_ENABLE || (OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE)
+        Case(kUriDuaRegistrationNotify, DuaManager);
+#endif
+
+#if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE
+        Case(kUriAnycastLocate, AnycastLocator);
+#endif
+
+        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
+#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
+        Case(kUriMlr, BackboneRouter::Manager);
+#endif
+#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
+        Case(kUriDuaRegistrationRequest, BackboneRouter::Manager);
+#endif
+#endif
+
+    default:
+        didHandle = false;
+        break;
+    }
+
+#undef Case
+
+    return didHandle;
 }
 
 Error Agent::Filter(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo, void *aContext)
@@ -117,5 +224,112 @@
     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)
+    : Coap::CoapSecure(aInstance)
+{
+    SetResourceHandler(&HandleResource);
+}
+
+bool SecureAgent::HandleResource(CoapBase               &aCoapBase,
+                                 const char             *aUriPath,
+                                 Message                &aMessage,
+                                 const Ip6::MessageInfo &aMessageInfo)
+{
+    return static_cast<SecureAgent &>(aCoapBase).HandleResource(aUriPath, aMessage, aMessageInfo);
+}
+
+bool SecureAgent::HandleResource(const char *aUriPath, Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    OT_UNUSED_VARIABLE(aMessage);
+    OT_UNUSED_VARIABLE(aMessageInfo);
+
+    bool didHandle = true;
+    Uri  uri       = UriFromPath(aUriPath);
+
+#define Case(kUri, Type)                                     \
+    case kUri:                                               \
+        Get<Type>().HandleTmf<kUri>(aMessage, aMessageInfo); \
+        break
+
+    switch (uri)
+    {
+#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE
+        Case(kUriJoinerFinalize, MeshCoP::Commissioner);
+#endif
+
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
+        Case(kUriCommissionerPetition, MeshCoP::BorderAgent);
+        Case(kUriCommissionerKeepAlive, MeshCoP::BorderAgent);
+        Case(kUriRelayTx, MeshCoP::BorderAgent);
+        Case(kUriCommissionerGet, MeshCoP::BorderAgent);
+        Case(kUriCommissionerSet, MeshCoP::BorderAgent);
+        Case(kUriActiveGet, MeshCoP::BorderAgent);
+        Case(kUriActiveSet, MeshCoP::BorderAgent);
+        Case(kUriPendingGet, MeshCoP::BorderAgent);
+        Case(kUriPendingSet, MeshCoP::BorderAgent);
+        Case(kUriProxyTx, MeshCoP::BorderAgent);
+#endif
+
+    default:
+        didHandle = false;
+        break;
+    }
+
+#undef Case
+
+    return didHandle;
+}
+
+#endif // OPENTHREAD_CONFIG_DTLS_ENABLE
+
 } // namespace Tmf
 } // namespace ot
diff --git a/src/core/thread/tmf.hpp b/src/core/thread/tmf.hpp
index 39e0185..02a1aa3 100644
--- a/src/core/thread/tmf.hpp
+++ b/src/core/thread/tmf.hpp
@@ -37,11 +37,26 @@
 #include "openthread-core-config.h"
 
 #include "coap/coap.hpp"
+#include "coap/coap_secure.hpp"
 #include "common/locator.hpp"
 
 namespace ot {
 namespace Tmf {
 
+/**
+ * This macro declares a TMF handler (a full template specialization of `HandleTmf<Uri>` method) in a given `Type`.
+ *
+ * The class `Type` MUST declare a template method of the following format:
+ *
+ *  template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+ *
+ * @param[in] Type      The `Type` in which the TMF handler is declared.
+ * @param[in] kUri      The `Uri` which is handled.
+ *
+ */
+#define DeclareTmfHandler(Type, kUri) \
+    template <> void Type::HandleTmf<kUri>(Coap::Message & aMessage, const Ip6::MessageInfo &aMessageInfo)
+
 constexpr uint16_t kUdpPort = 61631; ///< TMF UDP Port
 
 typedef Coap::Message Message; ///< A TMF message.
@@ -137,11 +152,7 @@
      * @param[in] aInstance      A reference to the OpenThread instance.
      *
      */
-    explicit Agent(Instance &aInstance)
-        : Coap::Coap(aInstance)
-    {
-        SetInterceptor(&Filter, this);
-    }
+    explicit Agent(Instance &aInstance);
 
     /**
      * This method starts the TMF agent.
@@ -171,10 +182,65 @@
      */
     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);
+
+    static bool HandleResource(CoapBase               &aCoapBase,
+                               const char             *aUriPath,
+                               Message                &aMessage,
+                               const Ip6::MessageInfo &aMessageInfo);
+    bool        HandleResource(const char *aUriPath, Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+
     static Error Filter(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo, void *aContext);
 };
 
+#if OPENTHREAD_CONFIG_DTLS_ENABLE
+
+/**
+ * This class implements functionality of the secure TMF agent.
+ *
+ */
+class SecureAgent : public Coap::CoapSecure
+{
+public:
+    /**
+     * This constructor initializes the object.
+     *
+     * @param[in] aInstance      A reference to the OpenThread instance.
+     *
+     */
+    explicit SecureAgent(Instance &aInstance);
+
+private:
+    static bool HandleResource(CoapBase               &aCoapBase,
+                               const char             *aUriPath,
+                               Message                &aMessage,
+                               const Ip6::MessageInfo &aMessageInfo);
+    bool        HandleResource(const char *aUriPath, Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+};
+
+#endif
+
 } // namespace Tmf
 } // namespace ot
 
diff --git a/src/core/thread/topology.cpp b/src/core/thread/topology.cpp
index 0eef0a0..ebb6c94 100644
--- a/src/core/thread/topology.cpp
+++ b/src/core/thread/topology.cpp
@@ -38,9 +38,33 @@
 #include "common/debug.hpp"
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
+#include "common/num_utils.hpp"
 
 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;
@@ -72,14 +96,19 @@
     mRloc16           = aNeighbor.GetRloc16();
     mLinkFrameCounter = aNeighbor.GetLinkFrameCounters().GetMaximum();
     mMleFrameCounter  = aNeighbor.GetMleFrameCounter();
-    mLinkQualityIn    = aNeighbor.GetLinkInfo().GetLinkQuality();
+    mLinkQualityIn    = aNeighbor.GetLinkQualityIn();
     mAverageRssi      = aNeighbor.GetLinkInfo().GetAverageRss();
     mLastRssi         = aNeighbor.GetLinkInfo().GetLastRss();
+    mLinkMargin       = aNeighbor.GetLinkInfo().GetLinkMargin();
     mFrameErrorRate   = aNeighbor.GetLinkInfo().GetFrameErrorRate();
     mMessageErrorRate = aNeighbor.GetLinkInfo().GetMessageErrorRate();
     mRxOnWhenIdle     = aNeighbor.IsRxOnWhenIdle();
     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)
@@ -173,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)
@@ -205,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)
 {
@@ -240,28 +269,32 @@
 void Child::Info::SetFrom(const Child &aChild)
 {
     Clear();
-    mExtAddress         = aChild.GetExtAddress();
-    mTimeout            = aChild.GetTimeout();
-    mRloc16             = aChild.GetRloc16();
-    mChildId            = Mle::Mle::ChildIdFromRloc16(aChild.GetRloc16());
-    mNetworkDataVersion = aChild.GetNetworkDataVersion();
-    mAge                = Time::MsecToSec(TimerMilli::GetNow() - aChild.GetLastHeard());
-    mLinkQualityIn      = aChild.GetLinkInfo().GetLinkQuality();
-    mAverageRssi        = aChild.GetLinkInfo().GetAverageRss();
-    mLastRssi           = aChild.GetLinkInfo().GetLastRss();
-    mFrameErrorRate     = aChild.GetLinkInfo().GetFrameErrorRate();
-    mMessageErrorRate   = aChild.GetLinkInfo().GetMessageErrorRate();
-    mQueuedMessageCnt   = aChild.GetIndirectMessageCount();
-    mVersion            = aChild.GetVersion();
-    mRxOnWhenIdle       = aChild.IsRxOnWhenIdle();
-    mFullThreadDevice   = aChild.IsFullThreadDevice();
-    mFullNetworkData    = (aChild.GetNetworkDataType() == NetworkData::kFullSet);
-    mIsStateRestoring   = aChild.IsStateRestoring();
+    mExtAddress          = aChild.GetExtAddress();
+    mTimeout             = aChild.GetTimeout();
+    mRloc16              = aChild.GetRloc16();
+    mChildId             = Mle::ChildIdFromRloc16(aChild.GetRloc16());
+    mNetworkDataVersion  = aChild.GetNetworkDataVersion();
+    mAge                 = Time::MsecToSec(TimerMilli::GetNow() - aChild.GetLastHeard());
+    mLinkQualityIn       = aChild.GetLinkQualityIn();
+    mAverageRssi         = aChild.GetLinkInfo().GetAverageRss();
+    mLastRssi            = aChild.GetLinkInfo().GetLastRss();
+    mFrameErrorRate      = aChild.GetLinkInfo().GetFrameErrorRate();
+    mMessageErrorRate    = aChild.GetLinkInfo().GetMessageErrorRate();
+    mQueuedMessageCnt    = aChild.GetIndirectMessageCount();
+    mVersion             = ClampToUint8(aChild.GetVersion());
+    mRxOnWhenIdle        = aChild.IsRxOnWhenIdle();
+    mFullThreadDevice    = aChild.IsFullThreadDevice();
+    mFullNetworkData     = (aChild.GetNetworkDataType() == NetworkData::kFullSet);
+    mIsStateRestoring    = aChild.IsStateRestoring();
+    mSupervisionInterval = aChild.GetSupervisionInterval();
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
     mIsCslSynced = aChild.IsCslSynchronized();
 #else
     mIsCslSynced = false;
 #endif
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    mConnectionTime = aChild.GetConnectionTime();
+#endif
 }
 
 const Ip6::Address *Child::AddressIterator::GetAddress(void) const
@@ -515,15 +548,25 @@
 {
     Clear();
     mRloc16          = aRouter.GetRloc16();
-    mRouterId        = Mle::Mle::RouterIdFromRloc16(mRloc16);
+    mRouterId        = Mle::RouterIdFromRloc16(mRloc16);
     mExtAddress      = aRouter.GetExtAddress();
     mAllocated       = true;
     mNextHop         = aRouter.GetNextHop();
     mLinkEstablished = aRouter.IsStateValid();
     mPathCost        = aRouter.GetCost();
-    mLinkQualityIn   = aRouter.GetLinkInfo().GetLinkQuality();
+    mLinkQualityIn   = aRouter.GetLinkQualityIn();
     mLinkQualityOut  = aRouter.GetLinkQualityOut();
     mAge             = static_cast<uint8_t>(Time::MsecToSec(TimerMilli::GetNow() - aRouter.GetLastHeard()));
+    mVersion         = ClampToUint8(aRouter.GetVersion());
+}
+
+void Router::Info::SetFrom(const Parent &aParent)
+{
+    SetFrom(static_cast<const Router &>(aParent));
+#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+    mCslClockAccuracy = aParent.GetCslAccuracy().GetClockAccuracy();
+    mCslUncertainty   = aParent.GetCslAccuracy().GetUncertainty();
+#endif
 }
 
 void Router::Clear(void)
@@ -534,4 +577,46 @@
     Init(instance);
 }
 
+LinkQuality Router::GetTwoWayLinkQuality(void) const { return Min(GetLinkQualityIn(), GetLinkQualityOut()); }
+
+void Router::SetFrom(const Parent &aParent)
+{
+    // We use an intermediate pointer to copy `aParent` to silence
+    // code checkers warning about object slicing (assigning a
+    // sub-class to base class instance).
+
+    const Router *parentAsRouter = &aParent;
+
+    *this = *parentAsRouter;
+}
+
+void Parent::Clear(void)
+{
+    Instance &instance = GetInstance();
+
+    memset(reinterpret_cast<void *>(this), 0, sizeof(Parent));
+    Init(instance);
+}
+
+bool Router::SetNextHopAndCost(uint8_t aNextHop, uint8_t aCost)
+{
+    bool changed = false;
+
+    if (mNextHop != aNextHop)
+    {
+        mNextHop = aNextHop;
+        changed  = true;
+    }
+
+    if (mCost != aCost)
+    {
+        mCost   = aCost;
+        changed = true;
+    }
+
+    return changed;
+}
+
+bool Router::SetNextHopToInvalid(void) { return SetNextHopAndCost(Mle::kInvalidRouterId, 0); }
+
 } // namespace ot
diff --git a/src/core/thread/topology.hpp b/src/core/thread/topology.hpp
index 7783565..62824f2 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"
@@ -59,6 +60,7 @@
 #include "thread/mle_types.hpp"
 #include "thread/network_data_types.hpp"
 #include "thread/radio_selector.hpp"
+#include "thread/version.hpp"
 
 namespace ot {
 
@@ -223,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.
@@ -363,10 +365,12 @@
     NetworkData::Type GetNetworkDataType(void) const { return GetDeviceMode().GetNetworkDataType(); }
 
     /**
-     * This method sets all bytes of the Extended Address to zero.
+     * This method returns the Extended Address.
+     *
+     * @returns A const reference to the Extended Address.
      *
      */
-    void ClearExtAddress(void) { memset(&mMacAddr, 0, sizeof(mMacAddr)); }
+    const Mac::ExtAddress &GetExtAddress(void) const { return mMacAddr; }
 
     /**
      * This method returns the Extended Address.
@@ -374,7 +378,7 @@
      * @returns A reference to the Extended Address.
      *
      */
-    const Mac::ExtAddress &GetExtAddress(void) const { return mMacAddr; }
+    Mac::ExtAddress &GetExtAddress(void) { return mMacAddr; }
 
     /**
      * This method sets the Extended Address.
@@ -539,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.
      *
@@ -557,7 +561,7 @@
      * @returns TRUE if neighbors is Thread 1.1, FALSE otherwise.
      *
      */
-    bool IsThreadVersion1p1(void) const { return mState != kStateInvalid && mVersion == OT_THREAD_VERSION_1_1; }
+    bool IsThreadVersion1p1(void) const { return mState != kStateInvalid && mVersion == kThreadVersion1p1; }
 
     /**
      * This method indicates whether or not neighbor is Thread 1.2 or higher..
@@ -565,7 +569,7 @@
      * @returns TRUE if neighbor is Thread 1.2 or higher, FALSE otherwise.
      *
      */
-    bool IsThreadVersion1p2OrHigher(void) const { return mState != kStateInvalid && mVersion >= OT_THREAD_VERSION_1_2; }
+    bool IsThreadVersion1p2OrHigher(void) const { return mState != kStateInvalid && mVersion >= kThreadVersion1p2; }
 
     /**
      * This method indicates whether Thread version supports CSL.
@@ -583,14 +587,14 @@
      */
     bool IsEnhancedKeepAliveSupported(void) const
     {
-        return mState != kStateInvalid && mVersion >= OT_THREAD_VERSION_1_2;
+        return (mState != kStateInvalid) && (mVersion >= kThreadVersion1p2);
     }
 
     /**
      * This method gets the device MLE version.
      *
      */
-    uint8_t GetVersion(void) const { return mVersion; }
+    uint16_t GetVersion(void) const { return mVersion; }
 
     /**
      * This method sets the device MLE version.
@@ -598,7 +602,7 @@
      * @param[in]  aVersion  The device MLE version.
      *
      */
-    void SetVersion(uint8_t aVersion) { mVersion = aVersion; }
+    void SetVersion(uint16_t aVersion) { mVersion = aVersion; }
 
     /**
      * This method gets the number of consecutive link failures.
@@ -637,6 +641,14 @@
     const LinkQualityInfo &GetLinkInfo(void) const { return mLinkInfo; }
 
     /**
+     * This method gets the link quality in value.
+     *
+     * @returns The link quality in value.
+     *
+     */
+    LinkQuality GetLinkQualityIn(void) const { return GetLinkInfo().GetLinkQuality(); }
+
+    /**
      * This method generates a new challenge value for MLE Link Request/Response exchanges.
      *
      */
@@ -658,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.
@@ -816,7 +838,7 @@
 #else
     uint8_t mLinkFailures; ///< Consecutive link failure count
 #endif
-    uint8_t         mVersion;  ///< The MLE version
+    uint16_t        mVersion;  ///< The MLE version
     LinkQualityInfo mLinkInfo; ///< Link quality info (contains average RSS, link margin and link quality)
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
     // A list of Link Metrics Forward Tracking Series that is being
@@ -829,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
@@ -970,7 +995,7 @@
          * @returns A reference to the `Ip6::Address` entry currently pointed by the iterator.
          *
          */
-        const Ip6::Address &operator*(void)const { return *GetAddress(); }
+        const Ip6::Address &operator*(void) const { return *GetAddress(); }
 
         /**
          * This method overloads operator `==` to evaluate whether or not two `Iterator` instances are equal.
@@ -1001,7 +1026,7 @@
 
         void Update(void);
 
-        const Child &            mChild;
+        const Child             &mChild;
         Ip6::Address::TypeFilter mFilter;
         Index                    mIndex;
         Ip6::Address             mMeshLocalAddress;
@@ -1201,7 +1226,21 @@
      */
     void SetRequestTlv(uint8_t aIndex, uint8_t aType) { mRequestTlvs[aIndex] = aType; }
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
+    /**
+     * This method returns the supervision interval (in seconds).
+     *
+     * @returns The supervision interval (in seconds).
+     *
+     */
+    uint16_t GetSupervisionInterval(void) const { return mSupervisionInterval; }
+
+    /**
+     * This method sets the supervision interval.
+     *
+     * @param[in] aInterval  The supervision interval (in seconds).
+     *
+     */
+    void SetSupervisionInterval(uint16_t aInterval) { mSupervisionInterval = aInterval; }
 
     /**
      * This method increments the number of seconds since last supervision of the child.
@@ -1223,13 +1262,11 @@
      */
     void ResetSecondsSinceLastSupervision(void) { mSecondsSinceSupervision = 0; }
 
-#endif // #if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
     /**
      * 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.
      *
@@ -1241,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.
@@ -1301,7 +1338,7 @@
         AddressIterator end(void) { return AddressIterator(mChild, AddressIterator::kEndIterator); }
 
     private:
-        const Child &            mChild;
+        const Child             &mChild;
         Ip6::Address::TypeFilter mFilter;
     };
 
@@ -1322,15 +1359,16 @@
         uint8_t mAttachChallenge[Mle::kMaxChallengeSize]; ///< The challenge value
     };
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-    uint16_t mSecondsSinceSupervision; ///< Number of seconds since last supervision of the child.
-#endif
+    uint16_t mSupervisionInterval;     // Supervision interval for the child (in sec).
+    uint16_t mSecondsSinceSupervision; // Number of seconds since last supervision of the child.
 
     static_assert(OPENTHREAD_CONFIG_NUM_MESSAGE_BUFFERS < 8192, "mQueuedMessageCount cannot fit max required!");
 };
 
 #endif // OPENTHREAD_FTD
 
+class Parent;
+
 /**
  * This class represents a Thread Router
  *
@@ -1352,6 +1390,14 @@
          *
          */
         void SetFrom(const Router &aRouter);
+
+        /**
+         * This method sets the `Info` instance from a given `Parent`.
+         *
+         * @param[in] aParent   A parent.
+         *
+         */
+        void SetFrom(const Parent &aParent);
     };
 
     /**
@@ -1360,14 +1406,7 @@
      * @param[in] aInstance  A reference to OpenThread instance.
      *
      */
-    void Init(Instance &aInstance)
-    {
-        Neighbor::Init(aInstance);
-#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-        SetCslClockAccuracy(kCslWorstCrystalPpm);
-        SetCslUncertainty(kCslWorstUncertainty);
-#endif
-    }
+    void Init(Instance &aInstance) { Neighbor::Init(aInstance); }
 
     /**
      * This method clears the router entry.
@@ -1376,6 +1415,12 @@
     void Clear(void);
 
     /**
+     * This method sets the `Router` entry from a `Parent`
+     *
+     */
+    void SetFrom(const Parent &aParent);
+
+    /**
      * This method gets the router ID of the next hop to this router.
      *
      * @returns The router ID of the next hop to this router.
@@ -1384,14 +1429,6 @@
     uint8_t GetNextHop(void) const { return mNextHop; }
 
     /**
-     * This method sets the router ID of the next hop to this router.
-     *
-     * @param[in]  aRouterId  The router ID of the next hop to this router.
-     *
-     */
-    void SetNextHop(uint8_t aRouterId) { mNextHop = aRouterId; }
-
-    /**
      * This method gets the link quality out value for this router.
      *
      * @returns The link quality out value for this router.
@@ -1408,6 +1445,14 @@
     void SetLinkQualityOut(LinkQuality aLinkQuality) { mLinkQualityOut = aLinkQuality; }
 
     /**
+     * This method gets the two-way link quality value (minimum of link quality in and out).
+     *
+     * @returns The two-way link quality value.
+     *
+     */
+    LinkQuality GetTwoWayLinkQuality(void) const;
+
+    /**
      * This method get the route cost to this router.
      *
      * @returns The route cost to this router.
@@ -1416,46 +1461,25 @@
     uint8_t GetCost(void) const { return mCost; }
 
     /**
-     * This method sets the router cost to this router.
+     * This method sets the next hop and cost to this router.
      *
-     * @param[in]  aCost  The router cost to this router.
+     * @param[in]  aNextHop  The Router ID of the next hop to this router.
+     * @param[in]  aCost     The cost to this router.
+     *
+     * @retval TRUE   If there was a change, i.e., @p aNextHop or @p aCost were different from their previous values.
+     * @retval FALSE  If no change to next hop and cost values (new values are the same as before).
      *
      */
-    void SetCost(uint8_t aCost) { mCost = aCost; }
-
-#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    /**
-     * This method get the CSL clock accuracy of this router.
-     *
-     * @returns The CSL clock accuracy of this router.
-     *
-     */
-    uint8_t GetCslClockAccuracy(void) const { return mCslClockAccuracy; }
+    bool SetNextHopAndCost(uint8_t aNextHop, uint8_t aCost);
 
     /**
-     * This method sets the CSL clock accuracy of this router.
+     * This method sets the next hop to this router as invalid and clears the cost.
      *
-     * @param[in]  aCslClockAccuracy  The CSL clock accuracy of this router.
+     * @retval TRUE   If there was a change (next hop was valid before).
+     * @retval FALSE  No change to next hop (next hop was invalid before).
      *
      */
-    void SetCslClockAccuracy(uint8_t aCslClockAccuracy) { mCslClockAccuracy = aCslClockAccuracy; }
-
-    /**
-     * This method get the CSL clock uncertainty of this router.
-     *
-     * @returns The CSL clock uncertainty of this router.
-     *
-     */
-    uint8_t GetCslUncertainty(void) const { return mCslUncertainty; }
-
-    /**
-     * This method sets the CSL clock uncertainty of this router.
-     *
-     * @param[in]  aCslUncertainty  The CSL clock uncertainty of this router.
-     *
-     */
-    void SetCslUncertainty(uint8_t aCslUncertainty) { mCslUncertainty = aCslUncertainty; }
-#endif
+    bool SetNextHopToInvalid(void);
 
 private:
     uint8_t mNextHop;            ///< The next hop towards this router
@@ -1466,9 +1490,73 @@
 #else
     uint8_t mCost : 4;     ///< The cost to this router via neighbor router
 #endif
+};
+
+/**
+ * This class represent parent of a child node.
+ *
+ */
+class Parent : public Router
+{
+public:
+    /**
+     * This method initializes the `Parent`.
+     *
+     * @param[in] aInstance  A reference to OpenThread instance.
+     *
+     */
+    void Init(Instance &aInstance)
+    {
+        Neighbor::Init(aInstance);
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    uint8_t mCslClockAccuracy; ///< Crystal accuracy, in units of ± ppm.
-    uint8_t mCslUncertainty;   ///< Scheduling uncertainty, in units of 10 us.
+        mCslAccuracy.Init();
+#endif
+    }
+
+    /**
+     * This method clears the parent entry.
+     *
+     */
+    void Clear(void);
+
+    /**
+     * This method gets route cost from parent to leader.
+     *
+     * @returns The route cost from parent to leader
+     *
+     */
+    uint8_t GetLeaderCost(void) const { return mLeaderCost; }
+
+    /**
+     * This method sets route cost from parent to leader.
+     *
+     * @param[in] aLaderConst  The route cost.
+     *
+     */
+    void SetLeaderCost(uint8_t aLeaderCost) { mLeaderCost = aLeaderCost; }
+
+#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+    /**
+     * This method gets the CSL accuracy (clock accuracy and uncertainty).
+     *
+     * @returns The CSL accuracy.
+     *
+     */
+    const Mac::CslAccuracy &GetCslAccuracy(void) const { return mCslAccuracy; }
+
+    /**
+     * This method sets CSL accuracy.
+     *
+     * @param[in] aCslAccuracy  The CSL accuracy.
+     *
+     */
+    void SetCslAccuracy(const Mac::CslAccuracy &aCslAccuracy) { mCslAccuracy = aCslAccuracy; }
+#endif
+
+private:
+    uint8_t mLeaderCost;
+#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+    Mac::CslAccuracy mCslAccuracy; // CSL accuracy (clock accuracy in ppm and uncertainty).
 #endif
 };
 
diff --git a/src/core/thread/uri_paths.cpp b/src/core/thread/uri_paths.cpp
index 9f72a6a..ff26d2e 100644
--- a/src/core/thread/uri_paths.cpp
+++ b/src/core/thread/uri_paths.cpp
@@ -33,46 +33,172 @@
 
 #include "uri_paths.hpp"
 
+#include "common/binary_search.hpp"
+#include "common/debug.hpp"
+#include "common/string.hpp"
+
+#include <string.h>
+
 namespace ot {
 
-const char UriPath::kAddressQuery[]           = "a/aq";
-const char UriPath::kAddressNotify[]          = "a/an";
-const char UriPath::kAddressError[]           = "a/ae";
-const char UriPath::kAddressRelease[]         = "a/ar";
-const char UriPath::kAddressSolicit[]         = "a/as";
-const char UriPath::kAnycastLocate[]          = "a/yl";
-const char UriPath::kActiveGet[]              = "c/ag";
-const char UriPath::kActiveSet[]              = "c/as";
-const char UriPath::kDatasetChanged[]         = "c/dc";
-const char UriPath::kEnergyScan[]             = "c/es";
-const char UriPath::kEnergyReport[]           = "c/er";
-const char UriPath::kPendingGet[]             = "c/pg";
-const char UriPath::kPendingSet[]             = "c/ps";
-const char UriPath::kServerData[]             = "a/sd";
-const char UriPath::kAnnounceBegin[]          = "c/ab";
-const char UriPath::kProxyRx[]                = "c/ur";
-const char UriPath::kProxyTx[]                = "c/ut";
-const char UriPath::kRelayRx[]                = "c/rx";
-const char UriPath::kRelayTx[]                = "c/tx";
-const char UriPath::kJoinerFinalize[]         = "c/jf";
-const char UriPath::kJoinerEntrust[]          = "c/je";
-const char UriPath::kLeaderPetition[]         = "c/lp";
-const char UriPath::kLeaderKeepAlive[]        = "c/la";
-const char UriPath::kPanIdConflict[]          = "c/pc";
-const char UriPath::kPanIdQuery[]             = "c/pq";
-const char UriPath::kCommissionerGet[]        = "c/cg";
-const char UriPath::kCommissionerKeepAlive[]  = "c/ca";
-const char UriPath::kCommissionerPetition[]   = "c/cp";
-const char UriPath::kCommissionerSet[]        = "c/cs";
-const char UriPath::kDiagnosticGetRequest[]   = "d/dg";
-const char UriPath::kDiagnosticGetQuery[]     = "d/dq";
-const char UriPath::kDiagnosticGetAnswer[]    = "d/da";
-const char UriPath::kDiagnosticReset[]        = "d/dr";
-const char UriPath::kMlr[]                    = "n/mr";
-const char UriPath::kDuaRegistrationRequest[] = "n/dr";
-const char UriPath::kDuaRegistrationNotify[]  = "n/dn";
-const char UriPath::kBackboneQuery[]          = "b/bq";
-const char UriPath::kBackboneAnswer[]         = "b/ba";
-const char UriPath::kBackboneMlr[]            = "b/bmr";
+namespace UriList {
+
+struct Entry
+{
+    const char *mPath;
+
+    constexpr static bool AreInOrder(const Entry &aFirst, const Entry &aSecond)
+    {
+        return AreStringsInOrder(aFirst.mPath, aSecond.mPath);
+    }
+
+    int Compare(const char *aPath) const { return strcmp(aPath, mPath); }
+};
+
+// The list of URI paths (MUST be sorted alphabetically)
+static constexpr Entry kEntries[] = {
+    {"a/ae"},  // (0) kUriAddressError
+    {"a/an"},  // (1) kUriAddressNotify
+    {"a/aq"},  // (2) kUriAddressQuery
+    {"a/ar"},  // (3) kUriAddressRelease
+    {"a/as"},  // (4) kUriAddressSolicit
+    {"a/sd"},  // (5) kUriServerData
+    {"a/yl"},  // (6) kUriAnycastLocate
+    {"b/ba"},  // (7) kUriBackboneAnswer
+    {"b/bmr"}, // (8) kUriBackboneMlr
+    {"b/bq"},  // (9) kUriBackboneQuery
+    {"c/ab"},  // (10) kUriAnnounceBegin
+    {"c/ag"},  // (11) kUriActiveGet
+    {"c/as"},  // (12) kUriActiveSet
+    {"c/ca"},  // (13) kUriCommissionerKeepAlive
+    {"c/cg"},  // (14) kUriCommissionerGet
+    {"c/cp"},  // (15) kUriCommissionerPetition
+    {"c/cs"},  // (16) kUriCommissionerSet
+    {"c/dc"},  // (17) kUriDatasetChanged
+    {"c/er"},  // (18) kUriEnergyReport
+    {"c/es"},  // (19) kUriEnergyScan
+    {"c/je"},  // (20) kUriJoinerEntrust
+    {"c/jf"},  // (21) kUriJoinerFinalize
+    {"c/la"},  // (22) kUriLeaderKeepAlive
+    {"c/lp"},  // (23) kUriLeaderPetition
+    {"c/pc"},  // (24) kUriPanIdConflict
+    {"c/pg"},  // (25) kUriPendingGet
+    {"c/pq"},  // (26) kUriPanIdQuery
+    {"c/ps"},  // (27) kUriPendingSet
+    {"c/rx"},  // (28) kUriRelayRx
+    {"c/tx"},  // (29) kUriRelayTx
+    {"c/ur"},  // (30) kUriProxyRx
+    {"c/ut"},  // (31) kUriProxyTx
+    {"d/da"},  // (32) kUriDiagnosticGetAnswer
+    {"d/dg"},  // (33) kUriDiagnosticGetRequest
+    {"d/dq"},  // (34) kUriDiagnosticGetQuery
+    {"d/dr"},  // (35) kUriDiagnosticReset
+    {"n/dn"},  // (36) kUriDuaRegistrationNotify
+    {"n/dr"},  // (37) kUriDuaRegistrationRequest
+    {"n/mr"},  // (38) kUriMlr
+};
+
+static_assert(BinarySearch::IsSorted(kEntries), "kEntries is not sorted");
+
+static_assert(0 == kUriAddressError, "kUriAddressError (`a/ae`) is invalid");
+static_assert(1 == kUriAddressNotify, "kUriAddressNotify (`a/an`) is invalid");
+static_assert(2 == kUriAddressQuery, "kUriAddressQuery (`a/aq`) is invalid");
+static_assert(3 == kUriAddressRelease, "kUriAddressRelease (`a/ar`) is invalid");
+static_assert(4 == kUriAddressSolicit, "kUriAddressSolicit (`a/as`) is invalid");
+static_assert(5 == kUriServerData, "kUriServerData (`a/sd`) is invalid");
+static_assert(6 == kUriAnycastLocate, "kUriAnycastLocate (`a/yl`) is invalid");
+static_assert(7 == kUriBackboneAnswer, "kUriBackboneAnswer (`b/ba`) is invalid");
+static_assert(8 == kUriBackboneMlr, "kUriBackboneMlr (`b/bmr`) is invalid");
+static_assert(9 == kUriBackboneQuery, "kUriBackboneQuery (`b/bq`) is invalid");
+static_assert(10 == kUriAnnounceBegin, "kUriAnnounceBegin (`c/ab`) is invalid");
+static_assert(11 == kUriActiveGet, "kUriActiveGet (`c/ag`) is invalid");
+static_assert(12 == kUriActiveSet, "kUriActiveSet (`c/as`) is invalid");
+static_assert(13 == kUriCommissionerKeepAlive, "kUriCommissionerKeepAlive (`c/ca`) is invalid");
+static_assert(14 == kUriCommissionerGet, "kUriCommissionerGet (`c/cg`) is invalid");
+static_assert(15 == kUriCommissionerPetition, "kUriCommissionerPetition (`c/cp`) is invalid");
+static_assert(16 == kUriCommissionerSet, "kUriCommissionerSet (`c/cs`) is invalid");
+static_assert(17 == kUriDatasetChanged, "kUriDatasetChanged (`c/dc`) is invalid");
+static_assert(18 == kUriEnergyReport, "kUriEnergyReport (`c/er`) is invalid");
+static_assert(19 == kUriEnergyScan, "kUriEnergyScan (`c/es`) is invalid");
+static_assert(20 == kUriJoinerEntrust, "kUriJoinerEntrust (`c/je`) is invalid");
+static_assert(21 == kUriJoinerFinalize, "kUriJoinerFinalize (`c/jf`) is invalid");
+static_assert(22 == kUriLeaderKeepAlive, "kUriLeaderKeepAlive (`c/la`) is invalid");
+static_assert(23 == kUriLeaderPetition, "kUriLeaderPetition (`c/lp`) is invalid");
+static_assert(24 == kUriPanIdConflict, "kUriPanIdConflict (`c/pc`) is invalid");
+static_assert(25 == kUriPendingGet, "kUriPendingGet (`c/pg`) is invalid");
+static_assert(26 == kUriPanIdQuery, "kUriPanIdQuery (`c/pq`) is invalid");
+static_assert(27 == kUriPendingSet, "kUriPendingSet (`c/ps`) is invalid");
+static_assert(28 == kUriRelayRx, "kUriRelayRx (`c/rx`) is invalid");
+static_assert(29 == kUriRelayTx, "kUriRelayTx (`c/tx`) is invalid");
+static_assert(30 == kUriProxyRx, "kUriProxyRx (`c/ur`) is invalid");
+static_assert(31 == kUriProxyTx, "kUriProxyTx (`c/ut`) is invalid");
+static_assert(32 == kUriDiagnosticGetAnswer, "kUriDiagnosticGetAnswer (`d/da`) is invalid");
+static_assert(33 == kUriDiagnosticGetRequest, "kUriDiagnosticGetRequest (`d/dg`) is invalid");
+static_assert(34 == kUriDiagnosticGetQuery, "kUriDiagnosticGetQuery (`d/dq`) is invalid");
+static_assert(35 == kUriDiagnosticReset, "kUriDiagnosticReset (`d/dr`) is invalid");
+static_assert(36 == kUriDuaRegistrationNotify, "kUriDuaRegistrationNotify (`n/dn`) is invalid");
+static_assert(37 == kUriDuaRegistrationRequest, "kUriDuaRegistrationRequest (`n/dr`) is invalid");
+static_assert(38 == kUriMlr, "kUriMlr (`n/mr`) is invalid");
+
+} // namespace UriList
+
+const char *PathForUri(Uri aUri)
+{
+    OT_ASSERT(aUri != kUriUnknown);
+
+    return UriList::kEntries[aUri].mPath;
+}
+
+Uri UriFromPath(const char *aPath)
+{
+    Uri                   uri   = kUriUnknown;
+    const UriList::Entry *entry = BinarySearch::Find(aPath, UriList::kEntries);
+
+    VerifyOrExit(entry != nullptr);
+    uri = static_cast<Uri>(entry - UriList::kEntries);
+
+exit:
+    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 30c7999..01ef7b0 100644
--- a/src/core/thread/uri_paths.hpp
+++ b/src/core/thread/uri_paths.hpp
@@ -36,56 +36,129 @@
 
 #include "openthread-core-config.h"
 
+#include "common/error.hpp"
+
 namespace ot {
 
 /**
- *
- * This structure contains Thread URI Path string definitions.
+ * This enumeration represents Thread URIs.
  *
  */
-struct UriPath
+enum Uri : uint8_t
 {
-    static const char kAddressQuery[];           ///< The URI Path for Address Query ("a/aq").
-    static const char kAddressNotify[];          ///< The URI Path for Address Notify ("a/an").
-    static const char kAddressError[];           ///< The URI Path for Address Error ("a/ae").
-    static const char kAddressRelease[];         ///< The URI Path for Address Release ("a/ar").
-    static const char kAddressSolicit[];         ///< The URI Path for Address Solicit ("a/as").
-    static const char kAnycastLocate[];          ///< The URI Path for Anycast Locate ("a/yl")
-    static const char kActiveGet[];              ///< The URI Path for MGMT_ACTIVE_GE ("c/ag")T
-    static const char kActiveSet[];              ///< The URI Path for MGMT_ACTIVE_SET ("c/as").
-    static const char kDatasetChanged[];         ///< The URI Path for MGMT_DATASET_CHANGED ("c/dc").
-    static const char kEnergyScan[];             ///< The URI Path for Energy Scan ("c/es").
-    static const char kEnergyReport[];           ///< The URI Path for Energy Report ("c/er").
-    static const char kPendingGet[];             ///< The URI Path for MGMT_PENDING_GET ("c/pg").
-    static const char kPendingSet[];             ///< The URI Path for MGMT_PENDING_SET ("c/ps").
-    static const char kServerData[];             ///< The URI Path for Server Data Registration ("a/sd").
-    static const char kAnnounceBegin[];          ///< The URI Path for Announce Begin ("c/ab").
-    static const char kProxyRx[];                ///< The URI Path for Proxy RX ("c/ur").
-    static const char kProxyTx[];                ///< The URI Path for Proxy TX ("c/ut").
-    static const char kRelayRx[];                ///< The URI Path for Relay RX ("c/rx").
-    static const char kRelayTx[];                ///< The URI Path for Relay TX ("c/tx").
-    static const char kJoinerFinalize[];         ///< The URI Path for Joiner Finalize ("c/jf").
-    static const char kJoinerEntrust[];          ///< The URI Path for Joiner Entrust ("c/je").
-    static const char kLeaderPetition[];         ///< The URI Path for Leader Petition ("c/lp").
-    static const char kLeaderKeepAlive[];        ///< The URI Path for Leader Keep Alive ("c/la").
-    static const char kPanIdConflict[];          ///< The URI Path for PAN ID Conflict ("c/pc").
-    static const char kPanIdQuery[];             ///< The URI Path for PAN ID Query ("c/pq").
-    static const char kCommissionerGet[];        ///< The URI Path for MGMT_COMMISSIONER_GET ("c/cg").
-    static const char kCommissionerKeepAlive[];  ///< The URI Path for Commissioner Keep Alive ("c/ca").
-    static const char kCommissionerPetition[];   ///< The URI Path for Commissioner Petition ("c/cp").
-    static const char kCommissionerSet[];        ///< The URI Path for MGMT_COMMISSIONER_SET ("c/cs").
-    static const char kDiagnosticGetRequest[];   ///< The URI Path for Network Diagnostic Get Request ("d/dg").
-    static const char kDiagnosticGetQuery[];     ///< The URI Path for Network Diagnostic Get Query ("d/dq").
-    static const char kDiagnosticGetAnswer[];    ///< The URI Path for Network Diagnostic Get Answer ("d/da").
-    static const char kDiagnosticReset[];        ///< The URI Path for Network Diagnostic Reset ("d/dr").
-    static const char kMlr[];                    ///< The URI Path for Multicast Listener Registration ("n/mr").
-    static const char kDuaRegistrationRequest[]; ///< The URI Path for DUA Registration Request ("n/dr").
-    static const char kDuaRegistrationNotify[];  ///< The URI Path for DUA Registration Notification ("n/dn").
-    static const char kBackboneQuery[];          ///< The URI Path for Backbone Query ("b/bq").
-    static const char kBackboneAnswer[];         ///< The URI Path for Backbone Answer / Backbone Notification ("b/ba").
-    static const char kBackboneMlr[];            ///< The URI Path for Backbone Multicast Listener Report ("b/bmr").
+    kUriAddressError,           ///< Address Error ("a/ae")
+    kUriAddressNotify,          ///< Address Notify ("a/an")
+    kUriAddressQuery,           ///< Address Query ("a/aq")
+    kUriAddressRelease,         ///< Address Release ("a/ar")
+    kUriAddressSolicit,         ///< Address Solicit ("a/as")
+    kUriServerData,             ///< Server Data Registration ("a/sd")
+    kUriAnycastLocate,          ///< Anycast Locate ("a/yl")
+    kUriBackboneAnswer,         ///< Backbone Answer / Backbone Notification ("b/ba")
+    kUriBackboneMlr,            ///< Backbone Multicast Listener Report ("b/bmr")
+    kUriBackboneQuery,          ///< Backbone Query ("b/bq")
+    kUriAnnounceBegin,          ///< Announce Begin ("c/ab")
+    kUriActiveGet,              ///< MGMT_ACTIVE_GET "c/ag"
+    kUriActiveSet,              ///< MGMT_ACTIVE_SET ("c/as")
+    kUriCommissionerKeepAlive,  ///< Commissioner Keep Alive ("c/ca")
+    kUriCommissionerGet,        ///< MGMT_COMMISSIONER_GET ("c/cg")
+    kUriCommissionerPetition,   ///< Commissioner Petition ("c/cp")
+    kUriCommissionerSet,        ///< MGMT_COMMISSIONER_SET ("c/cs")
+    kUriDatasetChanged,         ///< MGMT_DATASET_CHANGED ("c/dc")
+    kUriEnergyReport,           ///< Energy Report ("c/er")
+    kUriEnergyScan,             ///< Energy Scan ("c/es")
+    kUriJoinerEntrust,          ///< Joiner Entrust  ("c/je")
+    kUriJoinerFinalize,         ///< Joiner Finalize ("c/jf")
+    kUriLeaderKeepAlive,        ///< Leader Keep Alive ("c/la")
+    kUriLeaderPetition,         ///< Leader Petition ("c/lp")
+    kUriPanIdConflict,          ///< PAN ID Conflict ("c/pc")
+    kUriPendingGet,             ///< MGMT_PENDING_GET ("c/pg")
+    kUriPanIdQuery,             ///< PAN ID Query ("c/pq")
+    kUriPendingSet,             ///< MGMT_PENDING_SET ("c/ps")
+    kUriRelayRx,                ///< Relay RX ("c/rx")
+    kUriRelayTx,                ///< Relay TX ("c/tx")
+    kUriProxyRx,                ///< Proxy RX ("c/ur")
+    kUriProxyTx,                ///< Proxy TX ("c/ut")
+    kUriDiagnosticGetAnswer,    ///< Network Diagnostic Get Answer ("d/da")
+    kUriDiagnosticGetRequest,   ///< Network Diagnostic Get Request ("d/dg")
+    kUriDiagnosticGetQuery,     ///< Network Diagnostic Get Query ("d/dq")
+    kUriDiagnosticReset,        ///< Network Diagnostic Reset ("d/dr")
+    kUriDuaRegistrationNotify,  ///< DUA Registration Notification ("n/dn")
+    kUriDuaRegistrationRequest, ///< DUA Registration Request ("n/dr")
+    kUriMlr,                    ///< Multicast Listener Registration ("n/mr")
+    kUriUnknown,                ///< Unknown URI
 };
 
+/**
+ * This function returns URI path string for a given URI.
+ *
+ * @param[in] aUri   A URI.
+ *
+ * @returns The path string for @p aUri.
+ *
+ */
+const char *PathForUri(Uri aUri);
+
+/**
+ * This function looks up the URI from a given path string.
+ *
+ * @param[in] aPath    A path string.
+ *
+ * @returns The URI associated with @p aPath or `kUriUnknown` if no match is found.
+ *
+ */
+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/examples/platforms/cc2538/misc.c b/src/core/thread/version.hpp
similarity index 67%
rename from examples/platforms/cc2538/misc.c
rename to src/core/thread/version.hpp
index 94dca3d..c3584aa 100644
--- a/examples/platforms/cc2538/misc.c
+++ b/src/core/thread/version.hpp
@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2016, The OpenThread Authors.
+ *  Copyright (c) 2022, The OpenThread Authors.
  *  All rights reserved.
  *
  *  Redistribution and use in source and binary forms, with or without
@@ -26,24 +26,27 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#include <openthread/platform/misc.h>
+/**
+ * @file
+ *   This file includes definitions for Thread Version.
+ */
 
-#include "platform-cc2538.h"
+#ifndef VERSION_HPP_
+#define VERSION_HPP_
 
-void otPlatReset(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-    HWREG(SYS_CTRL_PWRDBG) = SYS_CTRL_PWRDBG_FORCE_WARM_RESET;
-}
+#include "openthread-core-config.h"
 
-otPlatResetReason otPlatGetResetReason(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-    // TODO: Write me!
-    return OT_PLAT_RESET_REASON_POWER_ON;
-}
+#include <stdint.h>
 
-void otPlatWakeHost(void)
-{
-    // TODO: implement an operation to wake the host from sleep state.
-}
+namespace ot {
+
+constexpr uint16_t kThreadVersion = OPENTHREAD_CONFIG_THREAD_VERSION; ///< Thread Version of this device.
+
+constexpr uint16_t kThreadVersion1p1   = OT_THREAD_VERSION_1_1;   ///< Thread Version 1.1
+constexpr uint16_t kThreadVersion1p2   = OT_THREAD_VERSION_1_2;   ///< Thread Version 1.2
+constexpr uint16_t kThreadVersion1p3   = OT_THREAD_VERSION_1_3;   ///< Thread Version 1.3
+constexpr uint16_t kThreadVersion1p3p1 = OT_THREAD_VERSION_1_3_1; ///< Thread Version 1.3.1
+
+} // namespace ot
+
+#endif // VERSION_HPP_
diff --git a/src/core/utils/channel_manager.cpp b/src/core/utils/channel_manager.cpp
index 93c24f5..1303509 100644
--- a/src/core/utils/channel_manager.cpp
+++ b/src/core/utils/channel_manager.cpp
@@ -57,7 +57,7 @@
     , mDelay(kMinimumDelay)
     , mChannel(0)
     , mState(kStateIdle)
-    , mTimer(aInstance, ChannelManager::HandleTimer)
+    , mTimer(aInstance)
     , mAutoSelectInterval(kDefaultAutoSelectInterval)
     , mAutoSelectEnabled(false)
     , mCcaFailureRateThreshold(kCcaFailureRateThreshold)
@@ -154,11 +154,6 @@
     StartAutoSelectTimer();
 }
 
-void ChannelManager::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<ChannelManager>().HandleTimer();
-}
-
 void ChannelManager::HandleTimer(void)
 {
     switch (mState)
@@ -191,8 +186,8 @@
 
     if (Get<ChannelMonitor>().GetSampleCount() <= kMinChannelMonitorSampleCount)
     {
-        LogInfo("Too few samples (%d <= %d) to select channel", Get<ChannelMonitor>().GetSampleCount(),
-                kMinChannelMonitorSampleCount);
+        LogInfo("Too few samples (%lu <= %lu) to select channel", ToUlong(Get<ChannelMonitor>().GetSampleCount()),
+                ToUlong(kMinChannelMonitorSampleCount));
         ExitNow(error = kErrorInvalidState);
     }
 
diff --git a/src/core/utils/channel_manager.hpp b/src/core/utils/channel_manager.hpp
index a6d1020..f375be0 100644
--- a/src/core/utils/channel_manager.hpp
+++ b/src/core/utils/channel_manager.hpp
@@ -276,7 +276,6 @@
     void        StartDatasetUpdate(void);
     static void HandleDatasetUpdateDone(Error aError, void *aContext);
     void        HandleDatasetUpdateDone(Error aError);
-    static void HandleTimer(Timer &aTimer);
     void        HandleTimer(void);
     void        StartAutoSelectTimer(void);
 
@@ -285,12 +284,14 @@
     bool  ShouldAttemptChannelChange(void);
 #endif
 
+    using ManagerTimer = TimerMilliIn<ChannelManager, &ChannelManager::HandleTimer>;
+
     Mac::ChannelMask mSupportedChannelMask;
     Mac::ChannelMask mFavoredChannelMask;
     uint16_t         mDelay;
     uint8_t          mChannel;
     State            mState;
-    TimerMilli       mTimer;
+    ManagerTimer     mTimer;
     uint32_t         mAutoSelectInterval;
     bool             mAutoSelectEnabled;
     uint16_t         mCcaFailureRateThreshold;
diff --git a/src/core/utils/channel_monitor.cpp b/src/core/utils/channel_monitor.cpp
index 8925c64..b707a79 100644
--- a/src/core/utils/channel_monitor.cpp
+++ b/src/core/utils/channel_monitor.cpp
@@ -64,7 +64,7 @@
     : InstanceLocator(aInstance)
     , mChannelMaskIndex(0)
     , mSampleCount(0)
-    , mTimer(aInstance, ChannelMonitor::HandleTimer)
+    , mTimer(aInstance)
 {
     memset(mChannelOccupancy, 0, sizeof(mChannelOccupancy));
 }
@@ -114,11 +114,6 @@
     return occupancy;
 }
 
-void ChannelMonitor::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<ChannelMonitor>().HandleTimer();
-}
-
 void ChannelMonitor::HandleTimer(void)
 {
     IgnoreError(Get<Mac::Mac>().EnergyScan(mScanChannelMasks[mChannelMaskIndex], 0,
@@ -158,7 +153,7 @@
 
         LogDebg("channel: %d, rssi:%d", aResult->mChannel, aResult->mMaxRssi);
 
-        if (aResult->mMaxRssi != OT_RADIO_RSSI_INVALID)
+        if (aResult->mMaxRssi != Radio::kInvalidRssi)
         {
             newValue = (aResult->mMaxRssi >= kRssiThreshold) ? kMaxOccupancy : 0;
         }
@@ -200,7 +195,7 @@
         logString.Append("%02x ", channel >> 8);
     }
 
-    LogInfo("%u [%s]", mSampleCount, logString.AsCString());
+    LogInfo("%lu [%s]", ToUlong(mSampleCount), logString.AsCString());
 #endif
 }
 
diff --git a/src/core/utils/channel_monitor.hpp b/src/core/utils/channel_monitor.hpp
index 8a51f43..b0221af 100644
--- a/src/core/utils/channel_monitor.hpp
+++ b/src/core/utils/channel_monitor.hpp
@@ -193,18 +193,19 @@
     static constexpr uint16_t kMaxJitterInterval = 4096;
     static constexpr uint32_t kMaxOccupancy      = 0xffff;
 
-    static void HandleTimer(Timer &aTimer);
     void        HandleTimer(void);
     static void HandleEnergyScanResult(Mac::EnergyScanResult *aResult, void *aContext);
     void        HandleEnergyScanResult(Mac::EnergyScanResult *aResult);
     void        LogResults(void);
 
+    using ScanTimer = TimerMilliIn<ChannelMonitor, &ChannelMonitor::HandleTimer>;
+
     static const uint32_t mScanChannelMasks[kNumChannelMasks];
 
-    uint8_t    mChannelMaskIndex : 3;
-    uint32_t   mSampleCount : 29;
-    uint16_t   mChannelOccupancy[kNumChannels];
-    TimerMilli mTimer;
+    uint8_t   mChannelMaskIndex : 3;
+    uint32_t  mSampleCount : 29;
+    uint16_t  mChannelOccupancy[kNumChannels];
+    ScanTimer mTimer;
 };
 
 /**
diff --git a/src/core/utils/heap.cpp b/src/core/utils/heap.cpp
index 0f8432d..02309f3 100644
--- a/src/core/utils/heap.cpp
+++ b/src/core/utils/heap.cpp
@@ -63,9 +63,9 @@
 
 void *Heap::CAlloc(size_t aCount, size_t aSize)
 {
-    void *   ret  = nullptr;
-    Block *  prev = nullptr;
-    Block *  curr = nullptr;
+    void    *ret  = nullptr;
+    Block   *prev = nullptr;
+    Block   *curr = nullptr;
     uint16_t size = static_cast<uint16_t>(aCount * aSize);
 
     VerifyOrExit(size);
diff --git a/src/core/utils/heap.hpp b/src/core/utils/heap.hpp
index 02b9a5e..bd391a9 100644
--- a/src/core/utils/heap.hpp
+++ b/src/core/utils/heap.hpp
@@ -209,7 +209,7 @@
      */
     bool IsClean(void) const
     {
-        Heap &       self  = *AsNonConst(this);
+        Heap        &self  = *AsNonConst(this);
         const Block &super = self.BlockSuper();
         const Block &first = self.BlockRight(super);
         return super.GetNext() == self.BlockOffset(first) && first.GetSize() == kFirstBlockSize;
@@ -227,7 +227,7 @@
     size_t GetFreeSize(void) const { return mMemory.mFreeSize; }
 
 private:
-#if OPENTHREAD_CONFIG_DTLS_ENABLE
+#if OPENTHREAD_CONFIG_TLS_ENABLE || OPENTHREAD_CONFIG_DTLS_ENABLE
     static constexpr uint16_t kMemorySize = OPENTHREAD_CONFIG_HEAP_INTERNAL_SIZE;
 #else
     static constexpr uint16_t kMemorySize = OPENTHREAD_CONFIG_HEAP_INTERNAL_SIZE_NO_DTLS;
diff --git a/src/core/utils/history_tracker.cpp b/src/core/utils/history_tracker.cpp
index 1991465..2bcda12 100644
--- a/src/core/utils/history_tracker.cpp
+++ b/src/core/utils/history_tracker.cpp
@@ -40,6 +40,7 @@
 #include "common/debug.hpp"
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
+#include "common/num_utils.hpp"
 #include "common/string.hpp"
 #include "common/timer.hpp"
 #include "net/ip6_headers.hpp"
@@ -52,17 +53,21 @@
 
 HistoryTracker::HistoryTracker(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mTimer(aInstance, HandleTimer)
+    , mTimer(aInstance)
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_DATA
     , mPreviousNetworkData(aInstance, mNetworkDataTlvBuffer, 0, sizeof(mNetworkDataTlvBuffer))
 #endif
 {
     mTimer.Start(kAgeCheckPeriod);
+
+#if OPENTHREAD_FTD && (OPENTHREAD_CONFIG_HISTORY_TRACKER_ROUTER_LIST_SIZE > 0)
+    memset(mRouterEntries, 0, sizeof(mRouterEntries));
+#endif
 }
 
 void HistoryTracker::RecordNetworkInfo(void)
 {
-    NetworkInfo *   entry = mNetInfoHistory.AddNewEntry();
+    NetworkInfo    *entry = mNetInfoHistory.AddNewEntry();
     Mle::DeviceMode mode;
 
     VerifyOrExit(entry != nullptr);
@@ -77,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;
@@ -120,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();
@@ -128,14 +133,14 @@
     entry->mChecksum             = headers.GetChecksum();
     entry->mIpProto              = headers.GetIpProto();
     entry->mIcmp6Type            = headers.IsIcmp6() ? headers.GetIcmpHeader().GetType() : 0;
-    entry->mAveRxRss             = (aType == kRxMessage) ? aMessage.GetRssAverager().GetAverage() : kInvalidRss;
+    entry->mAveRxRss             = (aType == kRxMessage) ? aMessage.GetRssAverager().GetAverage() : Radio::kInvalidRssi;
     entry->mLinkSecurity         = aMessage.IsLinkSecurityEnabled();
     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)
         {
@@ -278,6 +283,76 @@
     return;
 }
 
+#if OPENTHREAD_FTD
+void HistoryTracker::RecordRouterTableChange(void)
+{
+#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ROUTER_LIST_SIZE > 0
+
+    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+    {
+        RouterInfo   entry;
+        RouterEntry &oldEntry = mRouterEntries[routerId];
+
+        entry.mRouterId = routerId;
+
+        if (Get<RouterTable>().IsAllocated(routerId))
+        {
+            uint16_t nextHopRloc;
+            uint8_t  pathCost;
+
+            Get<RouterTable>().GetNextHopAndPathCost(Mle::Rloc16FromRouterId(routerId), nextHopRloc, pathCost);
+
+            entry.mNextHop  = (nextHopRloc == Mle::kInvalidRloc16) ? kNoNextHop : Mle::RouterIdFromRloc16(nextHopRloc);
+            entry.mPathCost = (pathCost < Mle::kMaxRouteCost) ? pathCost : 0;
+
+            if (!oldEntry.mIsAllocated)
+            {
+                entry.mEvent       = kRouterAdded;
+                entry.mOldPathCost = 0;
+            }
+            else if (oldEntry.mNextHop != entry.mNextHop)
+            {
+                entry.mEvent       = kRouterNextHopChanged;
+                entry.mOldPathCost = oldEntry.mPathCost;
+            }
+            else if ((entry.mNextHop != kNoNextHop) && (oldEntry.mPathCost != entry.mPathCost))
+            {
+                entry.mEvent       = kRouterCostChanged;
+                entry.mOldPathCost = oldEntry.mPathCost;
+            }
+            else
+            {
+                continue;
+            }
+
+            mRouterHistory.AddNewEntry(entry);
+
+            oldEntry.mIsAllocated = true;
+            oldEntry.mNextHop     = entry.mNextHop;
+            oldEntry.mPathCost    = entry.mPathCost;
+        }
+        else
+        {
+            // `routerId` is not allocated.
+
+            if (oldEntry.mIsAllocated)
+            {
+                entry.mEvent       = kRouterRemoved;
+                entry.mNextHop     = Mle::kInvalidRouterId;
+                entry.mOldPathCost = 0;
+                entry.mPathCost    = 0;
+
+                mRouterHistory.AddNewEntry(entry);
+
+                oldEntry.mIsAllocated = false;
+            }
+        }
+    }
+
+#endif // (OPENTHREAD_CONFIG_HISTORY_TRACKER_ROUTER_LIST_SIZE > 0)
+}
+#endif // OPENTHREAD_FTD
+
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_DATA
 void HistoryTracker::RecordNetworkDataChange(void)
 {
@@ -374,11 +449,6 @@
 #endif
 }
 
-void HistoryTracker::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<HistoryTracker>().HandleTimer();
-}
-
 void HistoryTracker::HandleTimer(void)
 {
     mNetInfoHistory.UpdateAgedEntries();
@@ -399,7 +469,7 @@
 
     if (aEntryAge >= kMaxAge)
     {
-        writer.Append("more than %u days", kMaxAge / Time::kOneDayInMsec);
+        writer.Append("more than %u days", static_cast<uint16_t>(kMaxAge / Time::kOneDayInMsec));
     }
     else
     {
@@ -407,14 +477,14 @@
 
         if (days > 0)
         {
-            writer.Append("%u day%s ", days, (days == 1) ? "" : "s");
+            writer.Append("%lu day%s ", ToUlong(days), (days == 1) ? "" : "s");
             aEntryAge -= days * Time::kOneDayInMsec;
         }
 
-        writer.Append("%02u:%02u:%02u.%03u", (aEntryAge / Time::kOneHourInMsec),
-                      (aEntryAge % Time::kOneHourInMsec) / Time::kOneMinuteInMsec,
-                      (aEntryAge % Time::kOneMinuteInMsec) / Time::kOneSecondInMsec,
-                      (aEntryAge % Time::kOneSecondInMsec));
+        writer.Append("%02u:%02u:%02u.%03u", static_cast<uint16_t>(aEntryAge / Time::kOneHourInMsec),
+                      static_cast<uint16_t>((aEntryAge % Time::kOneHourInMsec) / Time::kOneMinuteInMsec),
+                      static_cast<uint16_t>((aEntryAge % Time::kOneMinuteInMsec) / Time::kOneSecondInMsec),
+                      static_cast<uint16_t>(aEntryAge % Time::kOneSecondInMsec));
     }
 }
 
@@ -436,7 +506,7 @@
 
 uint32_t HistoryTracker::Timestamp::GetDurationTill(TimeMilli aTime) const
 {
-    return IsDistantPast() ? kMaxAge : OT_MIN(aTime - mTime, kMaxAge);
+    return IsDistantPast() ? kMaxAge : Min(aTime - mTime, kMaxAge);
 }
 
 //---------------------------------------------------------------------------------------------------------------------
@@ -473,9 +543,9 @@
 
 Error HistoryTracker::List::Iterate(uint16_t        aMaxSize,
                                     const Timestamp aTimestamps[],
-                                    Iterator &      aIterator,
-                                    uint16_t &      aListIndex,
-                                    uint32_t &      aEntryAge) const
+                                    Iterator       &aIterator,
+                                    uint16_t       &aListIndex,
+                                    uint32_t       &aEntryAge) const
 {
     Error error = kErrorNone;
 
diff --git a/src/core/utils/history_tracker.hpp b/src/core/utils/history_tracker.hpp
index 3a0a2ee..f1cabbf 100644
--- a/src/core/utils/history_tracker.hpp
+++ b/src/core/utils/history_tracker.hpp
@@ -54,6 +54,7 @@
 #include "thread/mle_types.hpp"
 #include "thread/neighbor_table.hpp"
 #include "thread/network_data.hpp"
+#include "thread/router_table.hpp"
 
 namespace ot {
 namespace Utils {
@@ -78,6 +79,9 @@
     friend class ot::Mle::Mle;
     friend class ot::NeighborTable;
     friend class ot::Ip6::Netif;
+#if OPENTHREAD_FTD
+    friend class ot::RouterTable;
+#endif
 
 public:
     /**
@@ -95,6 +99,14 @@
     static constexpr uint16_t kEntryAgeStringSize = OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE;
 
     /**
+     * This constants specified no next hop.
+     *
+     * Used for `mNextHop` in `RouteInfo` struture.
+     *
+     */
+    static constexpr uint8_t kNoNextHop = OT_HISTORY_TRACKER_NO_NEXT_HOP;
+
+    /**
      * This type represents an iterator to iterate through a history list.
      *
      */
@@ -125,6 +137,7 @@
     typedef otHistoryTrackerMulticastAddressInfo MulticastAddressInfo; ///< Multicast IPv6 address info.
     typedef otHistoryTrackerMessageInfo          MessageInfo;          ///< RX/TX IPv6 message info.
     typedef otHistoryTrackerNeighborInfo         NeighborInfo;         ///< Neighbor info.
+    typedef otHistoryTrackerRouterInfo           RouterInfo;           ///< Router info.
     typedef otHistoryTrackerOnMeshPrefixInfo     OnMeshPrefixInfo;     ///< Network Data on mesh prefix info.
     typedef otHistoryTrackerExternalRouteInfo    ExternalRouteInfo;    ///< Network Data external route info
 
@@ -226,6 +239,11 @@
         return mNeighborHistory.Iterate(aIterator, aEntryAge);
     }
 
+    const RouterInfo *IterateRouterHistory(Iterator &aIterator, uint32_t &aEntryAge) const
+    {
+        return mRouterHistory.Iterate(aIterator, aEntryAge);
+    }
+
     const OnMeshPrefixInfo *IterateOnMeshPrefixHistory(Iterator &aIterator, uint32_t &aEntryAge) const
     {
         return mOnMeshPrefixHistory.Iterate(aIterator, aEntryAge);
@@ -265,6 +283,7 @@
     static constexpr uint16_t kRxListSize            = OPENTHREAD_CONFIG_HISTORY_TRACKER_RX_LIST_SIZE;
     static constexpr uint16_t kTxListSize            = OPENTHREAD_CONFIG_HISTORY_TRACKER_TX_LIST_SIZE;
     static constexpr uint16_t kNeighborListSize      = OPENTHREAD_CONFIG_HISTORY_TRACKER_NEIGHBOR_LIST_SIZE;
+    static constexpr uint16_t kRouterListSize        = OPENTHREAD_CONFIG_HISTORY_TRACKER_ROUTER_LIST_SIZE;
     static constexpr uint16_t kOnMeshPrefixListSize  = OPENTHREAD_CONFIG_HISTORY_TRACKER_ON_MESH_PREFIX_LIST_SIZE;
     static constexpr uint16_t kExternalRouteListSize = OPENTHREAD_CONFIG_HISTORY_TRACKER_EXTERNAL_ROUTE_LIST_SIZE;
 
@@ -273,7 +292,6 @@
     static constexpr AddressEvent kAddressAdded   = OT_HISTORY_TRACKER_ADDRESS_EVENT_ADDED;
     static constexpr AddressEvent kAddressRemoved = OT_HISTORY_TRACKER_ADDRESS_EVENT_REMOVED;
 
-    static constexpr int8_t   kInvalidRss    = OT_RADIO_RSSI_INVALID;
     static constexpr uint16_t kInvalidRloc16 = Mac::kShortAddrInvalid;
 
     typedef otHistoryTrackerNeighborEvent NeighborEvent;
@@ -283,6 +301,13 @@
     static constexpr NeighborEvent kNeighborChanged   = OT_HISTORY_TRACKER_NEIGHBOR_EVENT_CHANGED;
     static constexpr NeighborEvent kNeighborRestoring = OT_HISTORY_TRACKER_NEIGHBOR_EVENT_RESTORING;
 
+    typedef otHistoryTrackerRouterEvent RouterEvent;
+
+    static constexpr RouterEvent kRouterAdded          = OT_HISTORY_TRACKER_ROUTER_EVENT_ADDED;
+    static constexpr RouterEvent kRouterRemoved        = OT_HISTORY_TRACKER_ROUTER_EVENT_REMOVED;
+    static constexpr RouterEvent kRouterNextHopChanged = OT_HISTORY_TRACKER_ROUTER_EVENT_NEXT_HOP_CHANGED;
+    static constexpr RouterEvent kRouterCostChanged    = OT_HISTORY_TRACKER_ROUTER_EVENT_COST_CHANGED;
+
     typedef otHistoryTrackerNetDataEvent NetDataEvent;
 
     static constexpr NetDataEvent kNetDataEntryAdded   = OT_HISTORY_TRACKER_NET_DATA_ENTRY_ADDED;
@@ -316,9 +341,9 @@
         uint16_t MapEntryNumberToListIndex(uint16_t aEntryNumber, uint16_t aMaxSize) const;
         Error    Iterate(uint16_t        aMaxSize,
                          const Timestamp aTimestamps[],
-                         Iterator &      aIterator,
-                         uint16_t &      aListIndex,
-                         uint32_t &      aEntryAge) const;
+                         Iterator       &aIterator,
+                         uint16_t       &aListIndex,
+                         uint32_t       &aEntryAge) const;
 
     private:
         uint16_t mStartIndex;
@@ -357,7 +382,7 @@
     public:
         void         Clear(void) {}
         uint16_t     GetSize(void) const { return 0; }
-        Entry *      AddNewEntry(void) { return nullptr; }
+        Entry       *AddNewEntry(void) { return nullptr; }
         void         AddNewEntry(const Entry &) {}
         const Entry *Iterate(Iterator &, uint32_t &) const { return nullptr; }
         void         RemoveAgedEntries(void) {}
@@ -379,32 +404,48 @@
         RecordMessage(aMessage, aMacDest, kTxMessage);
     }
 
-    void        RecordNetworkInfo(void);
-    void        RecordMessage(const Message &aMessage, const Mac::Address &aMacAddress, MessageType aType);
-    void        RecordNeighborEvent(NeighborTable::Event aEvent, const NeighborTable::EntryInfo &aInfo);
-    void        RecordAddressEvent(Ip6::Netif::AddressEvent aEvent, const Ip6::Netif::UnicastAddress &aUnicastAddress);
-    void        RecordAddressEvent(Ip6::Netif::AddressEvent            aEvent,
-                                   const Ip6::Netif::MulticastAddress &aMulticastAddress,
-                                   Ip6::Netif::AddressOrigin           aAddressOrigin);
-    void        HandleNotifierEvents(Events aEvents);
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
+    void RecordNetworkInfo(void);
+    void RecordMessage(const Message &aMessage, const Mac::Address &aMacAddress, MessageType aType);
+    void RecordNeighborEvent(NeighborTable::Event aEvent, const NeighborTable::EntryInfo &aInfo);
+    void RecordAddressEvent(Ip6::Netif::AddressEvent aEvent, const Ip6::Netif::UnicastAddress &aUnicastAddress);
+    void RecordAddressEvent(Ip6::Netif::AddressEvent            aEvent,
+                            const Ip6::Netif::MulticastAddress &aMulticastAddress,
+                            Ip6::Netif::AddressOrigin           aAddressOrigin);
+    void HandleNotifierEvents(Events aEvents);
+    void HandleTimer(void);
+#if OPENTHREAD_FTD
+    void RecordRouterTableChange(void);
+#endif
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_DATA
     void RecordNetworkDataChange(void);
     void RecordOnMeshPrefixEvent(NetDataEvent aEvent, const NetworkData::OnMeshPrefixConfig &aPrefix);
     void RecordExternalRouteEvent(NetDataEvent aEvent, const NetworkData::ExternalRouteConfig &aRoute);
 #endif
 
+    using TrackerTimer = TimerMilliIn<HistoryTracker, &HistoryTracker::HandleTimer>;
+
     EntryList<NetworkInfo, kNetInfoListSize>                mNetInfoHistory;
     EntryList<UnicastAddressInfo, kUnicastAddrListSize>     mUnicastAddressHistory;
     EntryList<MulticastAddressInfo, kMulticastAddrListSize> mMulticastAddressHistory;
     EntryList<MessageInfo, kRxListSize>                     mRxHistory;
     EntryList<MessageInfo, kTxListSize>                     mTxHistory;
     EntryList<NeighborInfo, kNeighborListSize>              mNeighborHistory;
+    EntryList<RouterInfo, kRouterListSize>                  mRouterHistory;
     EntryList<OnMeshPrefixInfo, kOnMeshPrefixListSize>      mOnMeshPrefixHistory;
     EntryList<ExternalRouteInfo, kExternalRouteListSize>    mExternalRouteHistory;
 
-    TimerMilli mTimer;
+    TrackerTimer mTimer;
+
+#if OPENTHREAD_FTD && (OPENTHREAD_CONFIG_HISTORY_TRACKER_ROUTER_LIST_SIZE > 0)
+    struct RouterEntry
+    {
+        bool    mIsAllocated : 1;
+        uint8_t mNextHop : 6;
+        uint8_t mPathCost : 4;
+    };
+
+    RouterEntry mRouterEntries[Mle::kMaxRouterId + 1];
+#endif
 
 #if OPENTHREAD_CONFIG_HISTORY_TRACKER_NET_DATA
     NetworkData::MutableNetworkData mPreviousNetworkData;
@@ -419,6 +460,7 @@
 DefineCoreType(otHistoryTrackerNetworkInfo, Utils::HistoryTracker::NetworkInfo);
 DefineCoreType(otHistoryTrackerMessageInfo, Utils::HistoryTracker::MessageInfo);
 DefineCoreType(otHistoryTrackerNeighborInfo, Utils::HistoryTracker::NeighborInfo);
+DefineCoreType(otHistoryTrackerRouterInfo, Utils::HistoryTracker::RouterInfo);
 DefineCoreType(otHistoryTrackerOnMeshPrefixInfo, Utils::HistoryTracker::OnMeshPrefixInfo);
 DefineCoreType(otHistoryTrackerExternalRouteInfo, Utils::HistoryTracker::ExternalRouteInfo);
 
diff --git a/src/core/utils/jam_detector.cpp b/src/core/utils/jam_detector.cpp
index ce4a4bc..a628fed 100644
--- a/src/core/utils/jam_detector.cpp
+++ b/src/core/utils/jam_detector.cpp
@@ -49,9 +49,7 @@
 
 JamDetector::JamDetector(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mHandler(nullptr)
-    , mContext(nullptr)
-    , mTimer(aInstance, JamDetector::HandleTimer)
+    , mTimer(aInstance)
     , mHistoryBitmap(0)
     , mCurSecondStartTime(0)
     , mSampleInterval(0)
@@ -71,8 +69,7 @@
     VerifyOrExit(!mEnabled, error = kErrorAlready);
     VerifyOrExit(aHandler != nullptr, error = kErrorInvalidArgs);
 
-    mHandler = aHandler;
-    mContext = aContext;
+    mCallback.Set(aHandler, aContext);
     mEnabled = true;
 
     LogInfo("Started");
@@ -161,11 +158,6 @@
     return error;
 }
 
-void JamDetector::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<JamDetector>().HandleTimer();
-}
-
 void JamDetector::HandleTimer(void)
 {
     int8_t rssi;
@@ -177,7 +169,7 @@
 
     // If the RSSI is valid, check if it exceeds the threshold
     // and try to update the history bit map
-    if (rssi != OT_RADIO_RSSI_INVALID)
+    if (rssi != Radio::kInvalidRssi)
     {
         didExceedThreshold = (rssi >= mRssiThreshold);
         UpdateHistory(didExceedThreshold);
@@ -268,7 +260,7 @@
 
     if (shouldInvokeHandler)
     {
-        mHandler(mJamState, mContext);
+        mCallback.Invoke(aNewState);
     }
 }
 
diff --git a/src/core/utils/jam_detector.hpp b/src/core/utils/jam_detector.hpp
index 98dc5e2..084ab77 100644
--- a/src/core/utils/jam_detector.hpp
+++ b/src/core/utils/jam_detector.hpp
@@ -40,6 +40,7 @@
 
 #include <stdint.h>
 
+#include "common/callback.hpp"
 #include "common/locator.hpp"
 #include "common/non_copyable.hpp"
 #include "common/notifier.hpp"
@@ -185,26 +186,26 @@
     static constexpr uint16_t kMinSampleInterval = 2;   // in ms
     static constexpr uint32_t kMaxRandomDelay    = 4;   // in ms
 
-    void        CheckState(void);
-    void        SetJamState(bool aNewState);
-    static void HandleTimer(Timer &aTimer);
-    void        HandleTimer(void);
-    void        UpdateHistory(bool aDidExceedThreshold);
-    void        UpdateJamState(void);
-    void        HandleNotifierEvents(Events aEvents);
+    void CheckState(void);
+    void SetJamState(bool aNewState);
+    void HandleTimer(void);
+    void UpdateHistory(bool aDidExceedThreshold);
+    void UpdateJamState(void);
+    void HandleNotifierEvents(Events aEvents);
 
-    Handler    mHandler;                  // Handler/callback to inform about jamming state
-    void *     mContext;                  // Context for handler/callback
-    TimerMilli mTimer;                    // RSSI sample timer
-    uint64_t   mHistoryBitmap;            // History bitmap, each bit correspond to 1 sec interval
-    TimeMilli  mCurSecondStartTime;       // Start time for current 1 sec interval
-    uint16_t   mSampleInterval;           // Current sample interval
-    uint8_t    mWindow : 6;               // Window (in sec) to monitor jamming
-    uint8_t    mBusyPeriod : 6;           // BusyPeriod (in sec) with mWindow to alert jamming
-    bool       mEnabled : 1;              // If jam detection is enabled
-    bool       mAlwaysAboveThreshold : 1; // State for current 1 sec interval
-    bool       mJamState : 1;             // Current jam state
-    int8_t     mRssiThreshold;            // RSSI threshold for jam detection
+    using SampleTimer = TimerMilliIn<JamDetector, &JamDetector::HandleTimer>;
+
+    Callback<Handler> mCallback;                 // Callback to inform about jamming state
+    SampleTimer       mTimer;                    // RSSI sample timer
+    uint64_t          mHistoryBitmap;            // History bitmap, each bit correspond to 1 sec interval
+    TimeMilli         mCurSecondStartTime;       // Start time for current 1 sec interval
+    uint16_t          mSampleInterval;           // Current sample interval
+    uint8_t           mWindow : 6;               // Window (in sec) to monitor jamming
+    uint8_t           mBusyPeriod : 6;           // BusyPeriod (in sec) with mWindow to alert jamming
+    bool              mEnabled : 1;              // If jam detection is enabled
+    bool              mAlwaysAboveThreshold : 1; // State for current 1 sec interval
+    bool              mJamState : 1;             // Current jam state
+    int8_t            mRssiThreshold;            // RSSI threshold for jam detection
 };
 
 /**
diff --git a/src/core/utils/mesh_diag.cpp b/src/core/utils/mesh_diag.cpp
new file mode 100644
index 0000000..f499329
--- /dev/null
+++ b/src/core/utils/mesh_diag.cpp
@@ -0,0 +1,309 @@
+/*
+ *  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 the Mesh Diag module.
+ */
+
+#include "mesh_diag.hpp"
+
+#if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
+
+#include "common/as_core_type.hpp"
+#include "common/code_utils.hpp"
+#include "common/debug.hpp"
+#include "common/instance.hpp"
+#include "common/locator_getters.hpp"
+#include "common/log.hpp"
+
+namespace ot {
+namespace Utils {
+
+using namespace ot::NetworkDiagnostic;
+
+RegisterLogModule("MeshDiag");
+
+//---------------------------------------------------------------------------------------------------------------------
+// MeshDiag
+
+MeshDiag::MeshDiag(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mTimer(aInstance)
+{
+}
+
+Error MeshDiag::DiscoverTopology(const DiscoverConfig &aConfig, DiscoverCallback aCallback, void *aContext)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(Get<Mle::Mle>().IsAttached(), error = kErrorInvalidState);
+    VerifyOrExit(!mTimer.IsRunning(), error = kErrorBusy);
+
+    Get<RouterTable>().GetRouterIdSet(mExpectedRouterIdSet);
+
+    for (uint8_t routerId = 0; routerId <= Mle::kMaxRouterId; routerId++)
+    {
+        if (mExpectedRouterIdSet.Contains(routerId))
+        {
+            SuccessOrExit(error = SendDiagGetTo(Mle::Rloc16FromRouterId(routerId), aConfig));
+        }
+    }
+
+    mDiscoverCallback.Set(aCallback, aContext);
+    mTimer.Start(kResponseTimeout);
+
+exit:
+    return error;
+}
+
+void MeshDiag::Cancel(void)
+{
+    mTimer.Stop();
+    IgnoreError(Get<Tmf::Agent>().AbortTransaction(HandleDiagGetResponse, this));
+}
+
+Error MeshDiag::SendDiagGetTo(uint16_t aRloc16, const DiscoverConfig &aConfig)
+{
+    static constexpr uint8_t kMaxTlvsToRequest = 6;
+
+    Error            error   = kErrorNone;
+    Coap::Message   *message = nullptr;
+    Tmf::MessageInfo messageInfo(GetInstance());
+    uint8_t          tlvs[kMaxTlvsToRequest];
+    uint8_t          tlvsLength = 0;
+
+    message = Get<Tmf::Agent>().NewConfirmablePostMessage(kUriDiagnosticGetRequest);
+    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
+
+    IgnoreError(message->SetPriority(Message::kPriorityLow));
+
+    tlvs[tlvsLength++] = Address16Tlv::kType;
+    tlvs[tlvsLength++] = ExtMacAddressTlv::kType;
+    tlvs[tlvsLength++] = RouteTlv::kType;
+    tlvs[tlvsLength++] = VersionTlv::kType;
+
+    if (aConfig.mDiscoverIp6Addresses)
+    {
+        tlvs[tlvsLength++] = Ip6AddressListTlv::kType;
+    }
+
+    if (aConfig.mDiscoverChildTable)
+    {
+        tlvs[tlvsLength++] = ChildTableTlv::kType;
+    }
+
+    SuccessOrExit(error = Tlv::Append<TypeListTlv>(*message, tlvs, tlvsLength));
+
+    messageInfo.SetSockAddrToRlocPeerAddrTo(aRloc16);
+    error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, HandleDiagGetResponse, this);
+
+exit:
+    FreeMessageOnError(message, error);
+    return error;
+}
+
+void MeshDiag::HandleDiagGetResponse(void                *aContext,
+                                     otMessage           *aMessage,
+                                     const otMessageInfo *aMessageInfo,
+                                     Error                aResult)
+{
+    static_cast<MeshDiag *>(aContext)->HandleDiagGetResponse(AsCoapMessagePtr(aMessage), AsCoreTypePtr(aMessageInfo),
+                                                             aResult);
+}
+
+void MeshDiag::HandleDiagGetResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult)
+{
+    OT_UNUSED_VARIABLE(aMessageInfo);
+
+    Error           error;
+    RouterInfo      routerInfo;
+    Ip6AddrIterator ip6AddrIterator;
+    ChildIterator   childIterator;
+
+    SuccessOrExit(aResult);
+    VerifyOrExit((aMessage != nullptr) && mTimer.IsRunning());
+
+    SuccessOrExit(routerInfo.ParseFrom(*aMessage));
+
+    if (ip6AddrIterator.InitFrom(*aMessage) == kErrorNone)
+    {
+        routerInfo.mIp6AddrIterator = &ip6AddrIterator;
+    }
+
+    if (childIterator.InitFrom(*aMessage, routerInfo.mRloc16) == kErrorNone)
+    {
+        routerInfo.mChildIterator = &childIterator;
+    }
+
+    mExpectedRouterIdSet.Remove(routerInfo.mRouterId);
+
+    if (mExpectedRouterIdSet.GetNumberOfAllocatedIds() == 0)
+    {
+        error = kErrorNone;
+        mTimer.Stop();
+    }
+    else
+    {
+        error = kErrorPending;
+    }
+
+    mDiscoverCallback.InvokeIfSet(error, &routerInfo);
+
+exit:
+    return;
+}
+
+void MeshDiag::HandleTimer(void)
+{
+    // Timed out waiting for response from one or more routers.
+
+    IgnoreError(Get<Tmf::Agent>().AbortTransaction(HandleDiagGetResponse, this));
+
+    mDiscoverCallback.InvokeIfSet(kErrorResponseTimeout, nullptr);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// MeshDiag::RouterInfo
+
+Error MeshDiag::RouterInfo::ParseFrom(const Message &aMessage)
+{
+    Error     error = kErrorNone;
+    Mle::Mle &mle   = aMessage.Get<Mle::Mle>();
+    RouteTlv  routeTlv;
+
+    Clear();
+
+    SuccessOrExit(error = Tlv::Find<Address16Tlv>(aMessage, mRloc16));
+    SuccessOrExit(error = Tlv::Find<ExtMacAddressTlv>(aMessage, AsCoreType(&mExtAddress)));
+    SuccessOrExit(error = Tlv::FindTlv(aMessage, routeTlv));
+
+    switch (error = Tlv::Find<VersionTlv>(aMessage, mVersion))
+    {
+    case kErrorNone:
+        break;
+    case kErrorNotFound:
+        mVersion = kVersionUnknown;
+        error    = kErrorNone;
+        break;
+    default:
+        ExitNow();
+    }
+
+    mRouterId           = Mle::RouterIdFromRloc16(mRloc16);
+    mIsThisDevice       = (mRloc16 == mle.GetRloc16());
+    mIsThisDeviceParent = mle.IsChild() && (mRloc16 == mle.GetParent().GetRloc16());
+    mIsLeader           = (mRouterId == mle.GetLeaderId());
+    mIsBorderRouter     = aMessage.Get<NetworkData::Leader>().ContainsBorderRouterWithRloc(mRloc16);
+
+    for (uint8_t id = 0, index = 0; id <= Mle::kMaxRouterId; id++)
+    {
+        if (routeTlv.IsRouterIdSet(id))
+        {
+            mLinkQualities[id] = routeTlv.GetLinkQualityIn(index);
+            index++;
+        }
+    }
+
+exit:
+    return error;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// MeshDiag::Ip6AddrIterator
+
+Error MeshDiag::Ip6AddrIterator::InitFrom(const Message &aMessage)
+{
+    Error    error;
+    uint16_t tlvLength;
+
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Ip6AddressListTlv::kType, mCurOffset, tlvLength));
+    mEndOffset = mCurOffset + tlvLength;
+    mMessage   = &aMessage;
+
+exit:
+    return error;
+}
+
+Error MeshDiag::Ip6AddrIterator::GetNextAddress(Ip6::Address &aAddress)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(mMessage != nullptr, error = kErrorNotFound);
+    VerifyOrExit(mCurOffset + sizeof(Ip6::Address) <= mEndOffset, error = kErrorNotFound);
+
+    IgnoreError(mMessage->Read(mCurOffset, aAddress));
+    mCurOffset += sizeof(Ip6::Address);
+
+exit:
+    return error;
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+// MeshDiag::ChildIterator
+
+Error MeshDiag::ChildIterator::InitFrom(const Message &aMessage, uint16_t aParentRloc16)
+{
+    Error    error;
+    uint16_t tlvLength;
+
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, ChildTableTlv::kType, mCurOffset, tlvLength));
+    mEndOffset    = mCurOffset + tlvLength;
+    mMessage      = &aMessage;
+    mParentRloc16 = aParentRloc16;
+
+exit:
+    return error;
+}
+
+Error MeshDiag::ChildIterator::GetNextChildInfo(ChildInfo &aChildInfo)
+{
+    Error           error = kErrorNone;
+    ChildTableEntry entry;
+
+    VerifyOrExit(mMessage != nullptr, error = kErrorNotFound);
+    VerifyOrExit(mCurOffset + sizeof(ChildTableEntry) <= mEndOffset, error = kErrorNotFound);
+
+    IgnoreError(mMessage->Read(mCurOffset, entry));
+    mCurOffset += sizeof(ChildTableEntry);
+
+    aChildInfo.mRloc16 = mParentRloc16 + entry.GetChildId();
+    entry.GetMode().Get(aChildInfo.mMode);
+    aChildInfo.mLinkQuality = entry.GetLinkQuality();
+
+    aChildInfo.mIsThisDevice   = (aChildInfo.mRloc16 == mMessage->Get<Mle::Mle>().GetRloc16());
+    aChildInfo.mIsBorderRouter = mMessage->Get<NetworkData::Leader>().ContainsBorderRouterWithRloc(aChildInfo.mRloc16);
+
+exit:
+    return error;
+}
+
+} // namespace Utils
+} // namespace ot
+
+#endif // #if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
diff --git a/src/core/utils/mesh_diag.hpp b/src/core/utils/mesh_diag.hpp
new file mode 100644
index 0000000..931a080
--- /dev/null
+++ b/src/core/utils/mesh_diag.hpp
@@ -0,0 +1,214 @@
+/*
+ *  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 definitions for Mesh Diagnostic module.
+ */
+
+#ifndef MESH_DIAG_HPP_
+#define MESH_DIAG_HPP_
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
+
+#include <openthread/mesh_diag.h>
+
+#include "coap/coap.hpp"
+#include "common/callback.hpp"
+#include "common/locator.hpp"
+#include "common/message.hpp"
+#include "common/timer.hpp"
+#include "net/ip6_address.hpp"
+#include "thread/network_diagnostic_tlvs.hpp"
+
+struct otMeshDiagIp6AddrIterator
+{
+};
+
+struct otMeshDiagChildIterator
+{
+};
+
+namespace ot {
+namespace Utils {
+
+/**
+ * This class implements the Mesh Diagnostics.
+ *
+ */
+class MeshDiag : public InstanceLocator
+{
+public:
+    static constexpr uint16_t kVersionUnknown = OT_MESH_DIAG_VERSION_UNKNOWN; ///< Unknown version.
+
+    typedef otMeshDiagDiscoverConfig   DiscoverConfig;   ///< The discovery configuration.
+    typedef otMeshDiagDiscoverCallback DiscoverCallback; ///< The discovery callback function pointer type.
+
+    /**
+     * This type represents an iterator to go over list of IPv6 addresses of a router.
+     *
+     */
+    class Ip6AddrIterator : public otMeshDiagIp6AddrIterator
+    {
+        friend class MeshDiag;
+
+    public:
+        /**
+         * This method iterates through the discovered IPv6 address of a router.
+         *
+         * @param[out]     aIp6Address  A reference to return the next IPv6 address (if any).
+         *
+         * @retval kErrorNone      Successfully retrieved the next address. @p aIp6Address is updated.
+         * @retval kErrorNotFound  No more address. Reached the end of the list.
+         *
+         */
+        Error GetNextAddress(Ip6::Address &aAddress);
+
+    private:
+        Error InitFrom(const Message &aMessage);
+
+        const Message *mMessage;
+        uint16_t       mCurOffset;
+        uint16_t       mEndOffset;
+    };
+
+    /**
+     * This type represents information about a router in Thread mesh.
+     *
+     */
+    class RouterInfo : public otMeshDiagRouterInfo, public Clearable<RouterInfo>
+    {
+        friend class MeshDiag;
+
+    private:
+        Error ParseFrom(const Message &aMessage);
+    };
+
+    /**
+     * This type represents information about a child in Thread mesh.
+     *
+     */
+    class ChildInfo : public otMeshDiagChildInfo, public Clearable<ChildInfo>
+    {
+    };
+
+    /**
+     * This type represents an iterator to go over list of IPv6 addresses of a router.
+     *
+     */
+    class ChildIterator : public otMeshDiagChildIterator
+    {
+        friend class MeshDiag;
+
+    public:
+        /**
+         * This method iterates through the discovered children of a router.
+         *
+         * @param[out]     aChildInfo  A reference to return the info for the next child (if any).
+         *
+         * @retval kErrorNone      Successfully retrieved the next child info. @p aChildInfo is updated.
+         * @retval kErrorNotFound  No more child entry. Reached the end of the list.
+         *
+         */
+        Error GetNextChildInfo(ChildInfo &aChildInfo);
+
+    private:
+        Error InitFrom(const Message &aMessage, uint16_t aParentRloc16);
+
+        const Message *mMessage;
+        uint16_t       mCurOffset;
+        uint16_t       mEndOffset;
+        uint16_t       mParentRloc16;
+    };
+
+    /**
+     * This constructor initializes the `MeshDiag` instance.
+     *
+     * @param[in] aInstance   The OpenThread instance.
+     *
+     */
+    explicit MeshDiag(Instance &aInstance);
+
+    /**
+     * This method starts network topology discovery.
+     *
+     * @param[in] aConfig          The configuration to use for discovery (e.g., which items to discover).
+     * @param[in] aCallback        The callback to report the discovered routers.
+     * @param[in] aContext         A context to pass in @p aCallback.
+     *
+     * @retval kErrorNone          The network topology discovery started successfully.
+     * @retval kErrorBusy          A previous discovery request is still ongoing.
+     * @retval kErrorInvalidState  Device is not attached.
+     * @retval kErrorNoBufs        Could not allocate buffer to send discovery messages.
+     *
+     */
+    Error DiscoverTopology(const DiscoverConfig &aConfig, DiscoverCallback aCallback, void *aContext);
+
+    /**
+     * This method cancels an ongoing topology discovery if there one, otherwise no action.
+     *
+     * When ongoing discovery is cancelled, the callback from `DiscoverTopology()` will not be called anymore.
+     *
+     */
+    void Cancel(void);
+
+private:
+    typedef ot::NetworkDiagnostic::Tlv Tlv;
+
+    static constexpr uint32_t kResponseTimeout = OPENTHREAD_CONFIG_MESH_DIAG_RESPONSE_TIMEOUT;
+
+    Error SendDiagGetTo(uint16_t aRloc16, const DiscoverConfig &aConfig);
+    void  HandleTimer(void);
+    void  HandleDiagGetResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult);
+
+    static void HandleDiagGetResponse(void                *aContext,
+                                      otMessage           *aMessage,
+                                      const otMessageInfo *aMessageInfo,
+                                      Error                aResult);
+
+    using TimeoutTimer = TimerMilliIn<MeshDiag, &MeshDiag::HandleTimer>;
+
+    Callback<DiscoverCallback> mDiscoverCallback;
+    Mle::RouterIdSet           mExpectedRouterIdSet;
+    TimeoutTimer               mTimer;
+};
+
+} // namespace Utils
+
+DefineCoreType(otMeshDiagIp6AddrIterator, Utils::MeshDiag::Ip6AddrIterator);
+DefineCoreType(otMeshDiagRouterInfo, Utils::MeshDiag::RouterInfo);
+DefineCoreType(otMeshDiagChildInfo, Utils::MeshDiag::ChildInfo);
+DefineCoreType(otMeshDiagChildIterator, Utils::MeshDiag::ChildIterator);
+
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_MESH_DIAG_ENABLE && OPENTHREAD_FTD
+
+#endif // MESH_DIAG_HPP_
diff --git a/src/core/utils/otns.cpp b/src/core/utils/otns.cpp
index 0030fba..f19185f 100644
--- a/src/core/utils/otns.cpp
+++ b/src/core/utils/otns.cpp
@@ -47,10 +47,7 @@
 
 const int kMaxStatusStringLength = 128;
 
-void Otns::EmitShortAddress(uint16_t aShortAddress)
-{
-    EmitStatus("rloc16=%d", aShortAddress);
-}
+void Otns::EmitShortAddress(uint16_t aShortAddress) { EmitStatus("rloc16=%d", aShortAddress); }
 
 void Otns::EmitExtendedAddress(const Mac::ExtAddress &aExtAddress)
 {
diff --git a/src/core/utils/parse_cmdline.cpp b/src/core/utils/parse_cmdline.cpp
index 52f77dd..3f30c83 100644
--- a/src/core/utils/parse_cmdline.cpp
+++ b/src/core/utils/parse_cmdline.cpp
@@ -44,15 +44,9 @@
 namespace Utils {
 namespace CmdLineParser {
 
-static bool IsSeparator(char aChar)
-{
-    return (aChar == ' ') || (aChar == '\t') || (aChar == '\r') || (aChar == '\n');
-}
+static bool IsSeparator(char aChar) { return (aChar == ' ') || (aChar == '\t') || (aChar == '\r') || (aChar == '\n'); }
 
-static bool IsEscapable(char aChar)
-{
-    return IsSeparator(aChar) || (aChar == '\\');
-}
+static bool IsEscapable(char aChar) { return IsSeparator(aChar) || (aChar == '\\'); }
 
 static Error ParseDigit(char aDigitChar, uint8_t &aValue)
 {
@@ -89,7 +83,7 @@
 {
     Error   error = kErrorNone;
     uint8_t index = 0;
-    char *  cmd;
+    char   *cmd;
 
     for (cmd = aCommandString; *cmd; cmd++)
     {
@@ -137,20 +131,11 @@
     return error;
 }
 
-Error ParseAsUint8(const char *aString, uint8_t &aUint8)
-{
-    return ParseUint<uint8_t>(aString, aUint8);
-}
+Error ParseAsUint8(const char *aString, uint8_t &aUint8) { return ParseUint<uint8_t>(aString, aUint8); }
 
-Error ParseAsUint16(const char *aString, uint16_t &aUint16)
-{
-    return ParseUint<uint16_t>(aString, aUint16);
-}
+Error ParseAsUint16(const char *aString, uint16_t &aUint16) { return ParseUint<uint16_t>(aString, aUint16); }
 
-Error ParseAsUint32(const char *aString, uint32_t &aUint32)
-{
-    return ParseUint<uint32_t>(aString, aUint32);
-}
+Error ParseAsUint32(const char *aString, uint32_t &aUint32) { return ParseUint<uint32_t>(aString, aUint32); }
 
 Error ParseAsUint64(const char *aString, uint64_t &aUint64)
 {
@@ -161,8 +146,8 @@
 
     enum : uint64_t
     {
-        kMaxHexBeforeOveflow = (0xffffffffffffffffULL / 16),
-        kMaxDecBeforeOverlow = (0xffffffffffffffffULL / 10),
+        kMaxHexBeforeOverflow = (0xffffffffffffffffULL / 16),
+        kMaxDecBeforeOverflow = (0xffffffffffffffffULL / 10),
     };
 
     VerifyOrExit(aString != nullptr, error = kErrorInvalidArgs);
@@ -179,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);
@@ -208,28 +193,22 @@
     return error;
 }
 
-Error ParseAsInt8(const char *aString, int8_t &aInt8)
-{
-    return ParseInt<int8_t>(aString, aInt8);
-}
+Error ParseAsInt8(const char *aString, int8_t &aInt8) { return ParseInt<int8_t>(aString, aInt8); }
 
-Error ParseAsInt16(const char *aString, int16_t &aInt16)
-{
-    return ParseInt<int16_t>(aString, aInt16);
-}
+Error ParseAsInt16(const char *aString, int16_t &aInt16) { return ParseInt<int16_t>(aString, aInt16); }
 
 Error ParseAsInt32(const char *aString, int32_t &aInt32)
 {
     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 == '+')
     {
@@ -237,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;
@@ -264,38 +243,20 @@
     return (aString != nullptr) ? otIp6AddressFromString(aString, &aAddress) : kErrorInvalidArgs;
 }
 
+Error ParseAsIp4Address(const char *aString, otIp4Address &aAddress)
+{
+    return (aString != nullptr) ? otIp4AddressFromString(aString, &aAddress) : kErrorInvalidArgs;
+}
+
 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.
 };
@@ -315,7 +276,7 @@
 
     switch (aMode)
     {
-    case kModeExtactSize:
+    case kModeExactSize:
         VerifyOrExit(expectedSize == aSize, error = kErrorInvalidArgs);
         break;
     case kModeUpToSize:
@@ -369,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)
@@ -385,15 +346,9 @@
 //---------------------------------------------------------------------------------------------------------------------
 // Arg class
 
-uint16_t Arg::GetLength(void) const
-{
-    return IsEmpty() ? 0 : static_cast<uint16_t>(strlen(mString));
-}
+uint16_t Arg::GetLength(void) const { return IsEmpty() ? 0 : static_cast<uint16_t>(strlen(mString)); }
 
-bool Arg::operator==(const char *aString) const
-{
-    return !IsEmpty() && (strcmp(mString, aString) == 0);
-}
+bool Arg::operator==(const char *aString) const { return !IsEmpty() && (strcmp(mString, aString) == 0); }
 
 void Arg::CopyArgsToStringArray(Arg aArgs[], char *aStrings[])
 {
diff --git a/src/core/utils/parse_cmdline.hpp b/src/core/utils/parse_cmdline.hpp
index 6b9025f..777a50a 100644
--- a/src/core/utils/parse_cmdline.hpp
+++ b/src/core/utils/parse_cmdline.hpp
@@ -38,7 +38,9 @@
 #include <string.h>
 
 #include <openthread/error.h>
+#include <openthread/instance.h>
 #include <openthread/ip6.h>
+#include <openthread/nat64.h>
 
 namespace ot {
 namespace Utils {
@@ -183,6 +185,18 @@
 otError ParseAsIp6Address(const char *aString, otIp6Address &aAddress);
 
 /**
+ * This function parses a string as an IPv4 address.
+ *
+ * @param[in]  aString   The string to parse.
+ * @param[out] aAddress  A reference to an `otIp6Address` to output the parsed IPv6 address.
+ *
+ * @retval kErrorNone         The string was parsed successfully.
+ * @retval kErrorInvalidArgs  The string does not contain valid IPv4 address.
+ *
+ */
+otError ParseAsIp4Address(const char *aString, otIp4Address &aAddress);
+
+/**
  * This function parses a string as an IPv6 prefix.
  *
  * The string is parsed as `{IPv6Address}/{PrefixLength}`.
@@ -277,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.
  *
@@ -486,6 +500,20 @@
     }
 
     /**
+     * This method parses the argument as an IPv4 address.
+     *
+     * @param[out] aAddress  A reference to an `otIp4Address` to output the parsed IPv4 address.
+     *
+     * @retval kErrorNone         The argument was parsed successfully.
+     * @retval kErrorInvalidArgs  The argument is empty or does not contain valid IPv4 address.
+     *
+     */
+    otError ParseAsIp4Address(otIp4Address &aAddress) const
+    {
+        return CmdLineParser::ParseAsIp4Address(mString, aAddress);
+    }
+
+    /**
      * This method parses the argument as an IPv6 prefix.
      *
      * The string is parsed as `{IPv6Address}/{PrefixLength}`.
@@ -629,57 +657,32 @@
 //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 // Specializations of `Arg::ParseAs<Type>()` method.
 
-template <> inline otError Arg::ParseAs(uint8_t &aValue) const
-{
-    return ParseAsUint8(aValue);
-}
+template <> inline otError Arg::ParseAs(uint8_t &aValue) const { return ParseAsUint8(aValue); }
 
-template <> inline otError Arg::ParseAs(uint16_t &aValue) const
-{
-    return ParseAsUint16(aValue);
-}
+template <> inline otError Arg::ParseAs(uint16_t &aValue) const { return ParseAsUint16(aValue); }
 
-template <> inline otError Arg::ParseAs(uint32_t &aValue) const
-{
-    return ParseAsUint32(aValue);
-}
+template <> inline otError Arg::ParseAs(uint32_t &aValue) const { return ParseAsUint32(aValue); }
 
-template <> inline otError Arg::ParseAs(uint64_t &aValue) const
-{
-    return ParseAsUint64(aValue);
-}
+template <> inline otError Arg::ParseAs(uint64_t &aValue) const { return ParseAsUint64(aValue); }
 
-template <> inline otError Arg::ParseAs(bool &aValue) const
-{
-    return ParseAsBool(aValue);
-}
+template <> inline otError Arg::ParseAs(bool &aValue) const { return ParseAsBool(aValue); }
 
-template <> inline otError Arg::ParseAs(int8_t &aValue) const
-{
-    return ParseAsInt8(aValue);
-}
+template <> inline otError Arg::ParseAs(int8_t &aValue) const { return ParseAsInt8(aValue); }
 
-template <> inline otError Arg::ParseAs(int16_t &aValue) const
-{
-    return ParseAsInt16(aValue);
-}
+template <> inline otError Arg::ParseAs(int16_t &aValue) const { return ParseAsInt16(aValue); }
 
-template <> inline otError Arg::ParseAs(int32_t &aValue) const
+template <> inline otError Arg::ParseAs(int32_t &aValue) const { return ParseAsInt32(aValue); }
+
+template <> inline otError Arg::ParseAs(const char *&aValue) const
 {
-    return ParseAsInt32(aValue);
+    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);
-}
+template <> inline otError Arg::ParseAs(otIp6Address &aValue) const { return ParseAsIp6Address(aValue); }
 
-template <> inline otError Arg::ParseAs(otIp6Prefix &aValue) const
-{
-    return ParseAsIp6Prefix(aValue);
-}
+template <> inline otError Arg::ParseAs(otIp6Prefix &aValue) const { return ParseAsIp6Prefix(aValue); }
 
 #endif
 
diff --git a/src/core/utils/ping_sender.cpp b/src/core/utils/ping_sender.cpp
index a0fe9c6..d1565ef 100644
--- a/src/core/utils/ping_sender.cpp
+++ b/src/core/utils/ping_sender.cpp
@@ -38,6 +38,7 @@
 #include "common/as_core_type.hpp"
 #include "common/encoding.hpp"
 #include "common/locator_getters.hpp"
+#include "common/num_utils.hpp"
 #include "common/random.hpp"
 
 namespace ot {
@@ -90,7 +91,7 @@
     : InstanceLocator(aInstance)
     , mIdentifier(0)
     , mTargetEchoSequence(0)
-    , mTimer(aInstance, PingSender::HandleTimer)
+    , mTimer(aInstance)
     , mIcmpHandler(PingSender::HandleIcmpReceive, this)
 {
     IgnoreError(Get<Ip6::Icmp>().RegisterHandler(mIcmpHandler));
@@ -126,7 +127,7 @@
 void PingSender::SendPing(void)
 {
     TimeMilli        now     = TimerMilli::GetNow();
-    Message *        message = nullptr;
+    Message         *message = nullptr;
     Ip6::MessageInfo messageInfo;
 
     messageInfo.SetSockAddr(mConfig.GetSource());
@@ -134,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())));
@@ -168,11 +169,6 @@
     }
 }
 
-void PingSender::HandleTimer(Timer &aTimer)
-{
-    aTimer.Get<PingSender>().HandleTimer();
-}
-
 void PingSender::HandleTimer(void)
 {
     if (mConfig.mCount > 0)
@@ -185,8 +181,8 @@
     }
 }
 
-void PingSender::HandleIcmpReceive(void *               aContext,
-                                   otMessage *          aMessage,
+void PingSender::HandleIcmpReceive(void                *aContext,
+                                   otMessage           *aMessage,
                                    const otMessageInfo *aMessageInfo,
                                    const otIcmp6Header *aIcmpHeader)
 {
@@ -194,8 +190,8 @@
                                                                 AsCoreType(aIcmpHeader));
 }
 
-void PingSender::HandleIcmpReceive(const Message &          aMessage,
-                                   const Ip6::MessageInfo & aMessageInfo,
+void PingSender::HandleIcmpReceive(const Message           &aMessage,
+                                   const Ip6::MessageInfo  &aMessageInfo,
                                    const Ip6::Icmp::Header &aIcmpHeader)
 {
     Reply    reply;
@@ -208,17 +204,16 @@
     SuccessOrExit(aMessage.Read(aMessage.GetOffset(), timestamp));
     timestamp = HostSwap32(timestamp);
 
-    reply.mSenderAddress = aMessageInfo.GetPeerAddr();
-    reply.mRoundTripTime =
-        static_cast<uint16_t>(OT_MIN(TimerMilli::GetNow() - TimeMilli(timestamp), NumericLimits<uint16_t>::kMax));
+    reply.mSenderAddress  = aMessageInfo.GetPeerAddr();
+    reply.mRoundTripTime  = ClampToUint16(TimerMilli::GetNow() - TimeMilli(timestamp));
     reply.mSize           = aMessage.GetLength() - aMessage.GetOffset();
     reply.mSequenceNumber = aIcmpHeader.GetSequence();
     reply.mHopLimit       = aMessageInfo.GetHopLimit();
 
     mStatistics.mReceivedCount++;
     mStatistics.mTotalRoundTripTime += reply.mRoundTripTime;
-    mStatistics.mMaxRoundTripTime = OT_MAX(mStatistics.mMaxRoundTripTime, reply.mRoundTripTime);
-    mStatistics.mMinRoundTripTime = OT_MIN(mStatistics.mMinRoundTripTime, reply.mRoundTripTime);
+    mStatistics.mMaxRoundTripTime = Max(mStatistics.mMaxRoundTripTime, reply.mRoundTripTime);
+    mStatistics.mMinRoundTripTime = Min(mStatistics.mMinRoundTripTime, reply.mRoundTripTime);
 
 #if OPENTHREAD_CONFIG_OTNS_ENABLE
     Get<Utils::Otns>().EmitPingReply(aMessageInfo.GetPeerAddr(), reply.mSize, timestamp, reply.mHopLimit);
diff --git a/src/core/utils/ping_sender.hpp b/src/core/utils/ping_sender.hpp
index aae63c7..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);
@@ -166,21 +166,22 @@
 
 private:
     void        SendPing(void);
-    static void HandleTimer(Timer &aTimer);
     void        HandleTimer(void);
-    static void HandleIcmpReceive(void *               aContext,
-                                  otMessage *          aMessage,
+    static void HandleIcmpReceive(void                *aContext,
+                                  otMessage           *aMessage,
                                   const otMessageInfo *aMessageInfo,
                                   const otIcmp6Header *aIcmpHeader);
-    void        HandleIcmpReceive(const Message &          aMessage,
-                                  const Ip6::MessageInfo & aMessageInfo,
+    void        HandleIcmpReceive(const Message           &aMessage,
+                                  const Ip6::MessageInfo  &aMessageInfo,
                                   const Ip6::Icmp::Header &aIcmpHeader);
 
+    using PingTimer = TimerMilliIn<PingSender, &PingSender::HandleTimer>;
+
     Config             mConfig;
     Statistics         mStatistics;
     uint16_t           mIdentifier;
     uint16_t           mTargetEchoSequence;
-    TimerMilli         mTimer;
+    PingTimer          mTimer;
     Ip6::Icmp::Handler mIcmpHandler;
 };
 
diff --git a/src/core/utils/power_calibration.cpp b/src/core/utils/power_calibration.cpp
new file mode 100644
index 0000000..0818d04
--- /dev/null
+++ b/src/core/utils/power_calibration.cpp
@@ -0,0 +1,245 @@
+/*
+ *  Copyright (c) 2022, 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 "power_calibration.hpp"
+
+#if OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+#include <openthread/platform/diag.h>
+
+#include "common/as_core_type.hpp"
+#include "common/code_utils.hpp"
+#include "common/locator_getters.hpp"
+
+namespace ot {
+namespace Utils {
+
+PowerCalibration::PowerCalibration(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mLastChannel(0)
+    , mCalibratedPowerIndex(kInvalidIndex)
+{
+    for (int16_t &targetPower : mTargetPowerTable)
+    {
+        targetPower = kInvalidPower;
+    }
+}
+
+void PowerCalibration::CalibratedPowerEntry::Init(int16_t        aActualPower,
+                                                  const uint8_t *aRawPowerSetting,
+                                                  uint16_t       aRawPowerSettingLength)
+{
+    AssertPointerIsNotNull(aRawPowerSetting);
+    OT_ASSERT(aRawPowerSettingLength <= kMaxRawPowerSettingSize);
+
+    mActualPower = aActualPower;
+    mLength      = aRawPowerSettingLength;
+    memcpy(mSettings, aRawPowerSetting, aRawPowerSettingLength);
+}
+
+Error PowerCalibration::CalibratedPowerEntry::GetRawPowerSetting(uint8_t  *aRawPowerSetting,
+                                                                 uint16_t *aRawPowerSettingLength)
+{
+    Error error = kErrorNone;
+
+    AssertPointerIsNotNull(aRawPowerSetting);
+    AssertPointerIsNotNull(aRawPowerSettingLength);
+    VerifyOrExit(*aRawPowerSettingLength >= mLength, error = kErrorInvalidArgs);
+
+    memcpy(aRawPowerSetting, mSettings, mLength);
+    *aRawPowerSettingLength = mLength;
+
+exit:
+    return error;
+}
+
+Error PowerCalibration::AddCalibratedPower(uint8_t        aChannel,
+                                           int16_t        aActualPower,
+                                           const uint8_t *aRawPowerSetting,
+                                           uint16_t       aRawPowerSettingLength)
+{
+    Error                error = kErrorNone;
+    CalibratedPowerEntry entry;
+    uint8_t              chIndex;
+
+    AssertPointerIsNotNull(aRawPowerSetting);
+    VerifyOrExit(IsChannelValid(aChannel) && aRawPowerSettingLength <= CalibratedPowerEntry::kMaxRawPowerSettingSize,
+                 error = kErrorInvalidArgs);
+
+    chIndex = aChannel - Radio::kChannelMin;
+    VerifyOrExit(!mCalibratedPowerTables[chIndex].ContainsMatching(aActualPower), error = kErrorInvalidArgs);
+    VerifyOrExit(!mCalibratedPowerTables[chIndex].IsFull(), error = kErrorNoBufs);
+
+    entry.Init(aActualPower, aRawPowerSetting, aRawPowerSettingLength);
+    SuccessOrExit(error = mCalibratedPowerTables[chIndex].PushBack(entry));
+
+    if (aChannel == mLastChannel)
+    {
+        mCalibratedPowerIndex = kInvalidIndex;
+    }
+
+exit:
+    return error;
+}
+
+void PowerCalibration::ClearCalibratedPowers(void)
+{
+    for (CalibratedPowerTable &table : mCalibratedPowerTables)
+    {
+        table.Clear();
+    }
+
+    mCalibratedPowerIndex = kInvalidIndex;
+}
+
+Error PowerCalibration::SetChannelTargetPower(uint8_t aChannel, int16_t aTargetPower)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(IsChannelValid(aChannel), error = kErrorInvalidArgs);
+    mTargetPowerTable[aChannel - Radio::kChannelMin] = aTargetPower;
+
+    if (aChannel == mLastChannel)
+    {
+        mCalibratedPowerIndex = kInvalidIndex;
+    }
+
+exit:
+    return error;
+}
+
+Error PowerCalibration::GetPowerSettings(uint8_t   aChannel,
+                                         int16_t  *aTargetPower,
+                                         int16_t  *aActualPower,
+                                         uint8_t  *aRawPowerSetting,
+                                         uint16_t *aRawPowerSettingLength)
+{
+    Error   error = kErrorNone;
+    uint8_t chIndex;
+    uint8_t powerIndex = kInvalidIndex;
+    int16_t foundPower = kInvalidPower;
+    int16_t targetPower;
+    int16_t actualPower;
+
+    VerifyOrExit(IsChannelValid(aChannel), error = kErrorInvalidArgs);
+    VerifyOrExit((mLastChannel != aChannel) || IsPowerUpdated());
+
+    chIndex     = aChannel - Radio::kChannelMin;
+    targetPower = mTargetPowerTable[chIndex];
+    VerifyOrExit(targetPower != kInvalidPower, error = kErrorNotFound);
+
+    for (uint8_t i = 0; i < mCalibratedPowerTables[chIndex].GetLength(); i++)
+    {
+        actualPower = mCalibratedPowerTables[chIndex][i].GetActualPower();
+
+        if ((actualPower <= targetPower) && ((foundPower == kInvalidPower) || (foundPower <= actualPower)))
+        {
+            foundPower = actualPower;
+            powerIndex = i;
+        }
+    }
+
+    VerifyOrExit(powerIndex != kInvalidIndex, error = kErrorNotFound);
+
+    mCalibratedPowerIndex = powerIndex;
+    mLastChannel          = aChannel;
+
+exit:
+    if (error == kErrorNone)
+    {
+        chIndex = mLastChannel - Radio::kChannelMin;
+
+        if (aTargetPower != nullptr)
+        {
+            *aTargetPower = mTargetPowerTable[chIndex];
+        }
+
+        if (aActualPower != nullptr)
+        {
+            *aActualPower = mCalibratedPowerTables[chIndex][mCalibratedPowerIndex].GetActualPower();
+        }
+
+        error = mCalibratedPowerTables[chIndex][mCalibratedPowerIndex].GetRawPowerSetting(aRawPowerSetting,
+                                                                                          aRawPowerSettingLength);
+    }
+
+    return error;
+}
+} // namespace Utils
+} // namespace ot
+
+using namespace ot;
+
+otError otPlatRadioAddCalibratedPower(otInstance    *aInstance,
+                                      uint8_t        aChannel,
+                                      int16_t        aActualPower,
+                                      const uint8_t *aRawPowerSetting,
+                                      uint16_t       aRawPowerSettingLength)
+{
+    return AsCoreType(aInstance).Get<Utils::PowerCalibration>().AddCalibratedPower(
+        aChannel, aActualPower, aRawPowerSetting, aRawPowerSettingLength);
+}
+
+otError otPlatRadioClearCalibratedPowers(otInstance *aInstance)
+{
+    AsCoreType(aInstance).Get<Utils::PowerCalibration>().ClearCalibratedPowers();
+    return OT_ERROR_NONE;
+}
+
+otError otPlatRadioSetChannelTargetPower(otInstance *aInstance, uint8_t aChannel, int16_t aTargetPower)
+{
+    return AsCoreType(aInstance).Get<Utils::PowerCalibration>().SetChannelTargetPower(aChannel, aTargetPower);
+}
+
+otError otPlatRadioGetRawPowerSetting(otInstance *aInstance,
+                                      uint8_t     aChannel,
+                                      uint8_t    *aRawPowerSetting,
+                                      uint16_t   *aRawPowerSettingLength)
+{
+    AssertPointerIsNotNull(aRawPowerSetting);
+    AssertPointerIsNotNull(aRawPowerSettingLength);
+
+    return AsCoreType(aInstance).Get<Utils::PowerCalibration>().GetPowerSettings(
+        aChannel, nullptr, nullptr, aRawPowerSetting, aRawPowerSettingLength);
+}
+
+otError otPlatDiagRadioGetPowerSettings(otInstance *aInstance,
+                                        uint8_t     aChannel,
+                                        int16_t    *aTargetPower,
+                                        int16_t    *aActualPower,
+                                        uint8_t    *aRawPowerSetting,
+                                        uint16_t   *aRawPowerSettingLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    AssertPointerIsNotNull(aRawPowerSetting);
+    AssertPointerIsNotNull(aRawPowerSettingLength);
+
+    return AsCoreType(aInstance).Get<Utils::PowerCalibration>().GetPowerSettings(
+        aChannel, aTargetPower, aActualPower, aRawPowerSetting, aRawPowerSettingLength);
+}
+#endif // OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
diff --git a/src/core/utils/power_calibration.hpp b/src/core/utils/power_calibration.hpp
new file mode 100644
index 0000000..e16775b
--- /dev/null
+++ b/src/core/utils/power_calibration.hpp
@@ -0,0 +1,177 @@
+/*
+ *  Copyright (c) 2022, 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 includes definitions for the platform power calibration module.
+ *
+ */
+#ifndef POWER_CALIBRATION_HPP_
+#define POWER_CALIBRATION_HPP_
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+
+#include <openthread/platform/radio.h>
+
+#include "common/array.hpp"
+#include "common/numeric_limits.hpp"
+#include "radio/radio.hpp"
+
+namespace ot {
+namespace Utils {
+
+/**
+ * This class implements power calibration module.
+ *
+ * The power calibration module implements the radio platform power calibration APIs. It mainly stores the calibrated
+ * power table and the target power table, provides an API for the platform to get the raw power setting of the
+ * specified channel.
+ *
+ */
+class PowerCalibration : public InstanceLocator, private NonCopyable
+{
+public:
+    explicit PowerCalibration(Instance &aInstance);
+
+    /**
+     * 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.
+     * @param[in] aRawPowerSetting        A pointer to the raw power setting byte array.
+     * @param[in] aRawPowerSettingLength  The length of the @p aRawPowerSetting.
+     *
+     * @retval kErrorNone         Successfully added the calibrated power to the power calibration table.
+     * @retval kErrorNoBufs       No available entry in the power calibration table.
+     * @retval kErrorInvalidArgs  The @p aChannel, @p aActualPower or @p aRawPowerSetting is invalid or the
+     *                            @ aActualPower already exists in the power calibration table.
+     *
+     */
+    Error AddCalibratedPower(uint8_t        aChannel,
+                             int16_t        aActualPower,
+                             const uint8_t *aRawPowerSetting,
+                             uint16_t       aRawPowerSettingLength);
+
+    /**
+     * Clear all calibrated powers from the power calibration table.
+     *
+     */
+    void ClearCalibratedPowers(void);
+
+    /**
+     * Set the target power for the given channel.
+     *
+     * @param[in]  aChannel      The radio channel.
+     * @param[in]  aTargetPower  The target power in 0.01dBm. Passing `INT16_MAX` will disable this channel.
+     *
+     * @retval  kErrorNone         Successfully set the target power.
+     * @retval  kErrorInvalidArgs  The @p aChannel or @p aTargetPower is invalid.
+     *
+     */
+    Error SetChannelTargetPower(uint8_t aChannel, int16_t aTargetPower);
+
+    /**
+     * Get the power settings for the given channel.
+     *
+     * Platform radio layer should parse the raw power setting based on the radio layer defined format and set the
+     * parameters of each radio hardware module.
+     *
+     * @param[in]      aChannel                The radio channel.
+     * @param[out]     aTargetPower            A pointer to the target power in 0.01 dBm. May be set to nullptr if
+     *                                         the caller doesn't want to get the target power.
+     * @param[out]     aActualPower            A pointer to the actual power in 0.01 dBm. May be set to nullptr if
+     *                                         the caller doesn't want to get the actual power.
+     * @param[out]     aRawPowerSetting        A pointer to the raw power setting byte array.
+     * @param[in,out]  aRawPowerSettingLength  On input, a pointer to the size of @p aRawPowerSetting.
+     *                                         On output, a pointer to the length of the raw power setting data.
+     *
+     * @retval  kErrorNone         Successfully got the target power.
+     * @retval  kErrorInvalidArgs  The @p aChannel is invalid, @p aRawPowerSetting or @p aRawPowerSettingLength is
+     *                             nullptr or @aRawPowerSettingLength is too short.
+     * @retval  kErrorNotFound     The power settings for the @p aChannel was not found.
+     *
+     */
+    Error GetPowerSettings(uint8_t   aChannel,
+                           int16_t  *aTargetPower,
+                           int16_t  *aActualPower,
+                           uint8_t  *aRawPowerSetting,
+                           uint16_t *aRawPowerSettingLength);
+
+private:
+    class CalibratedPowerEntry
+    {
+    public:
+        static constexpr uint16_t kMaxRawPowerSettingSize = OPENTHREAD_CONFIG_POWER_CALIBRATION_RAW_POWER_SETTING_SIZE;
+
+        CalibratedPowerEntry(void)
+            : mActualPower(kInvalidPower)
+            , mLength(0)
+        {
+        }
+
+        void    Init(int16_t aActualPower, const uint8_t *aRawPowerSetting, uint16_t aRawPowerSettingLength);
+        Error   GetRawPowerSetting(uint8_t *aRawPowerSetting, uint16_t *aRawPowerSettingLength);
+        int16_t GetActualPower(void) const { return mActualPower; }
+        bool    Matches(int16_t aActualPower) const { return aActualPower == mActualPower; }
+
+    private:
+        int16_t  mActualPower;
+        uint8_t  mSettings[kMaxRawPowerSettingSize];
+        uint16_t mLength;
+    };
+
+    bool IsPowerUpdated(void) const { return mCalibratedPowerIndex == kInvalidIndex; }
+    bool IsChannelValid(uint8_t aChannel) const
+    {
+        return ((aChannel >= Radio::kChannelMin) && (aChannel <= Radio::kChannelMax));
+    }
+
+    static constexpr uint8_t  kInvalidIndex = NumericLimits<uint8_t>::kMax;
+    static constexpr uint16_t kInvalidPower = NumericLimits<int16_t>::kMax;
+    static constexpr uint16_t kMaxNumCalibratedPowers =
+        OPENTHREAD_CONFIG_POWER_CALIBRATION_NUM_CALIBRATED_POWER_ENTRIES;
+    static constexpr uint16_t kNumChannels = Radio::kChannelMax - Radio::kChannelMin + 1;
+
+    static_assert(kMaxNumCalibratedPowers < NumericLimits<uint8_t>::kMax,
+                  "kMaxNumCalibratedPowers is larger than or equal to max");
+
+    typedef Array<CalibratedPowerEntry, kMaxNumCalibratedPowers> CalibratedPowerTable;
+
+    uint8_t              mLastChannel;
+    int16_t              mTargetPowerTable[kNumChannels];
+    uint8_t              mCalibratedPowerIndex;
+    CalibratedPowerTable mCalibratedPowerTables[kNumChannels];
+};
+} // namespace Utils
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+#endif // POWER_CALIBRATION_HPP_
diff --git a/src/core/utils/slaac_address.cpp b/src/core/utils/slaac_address.cpp
index 82cadfc..825130a 100644
--- a/src/core/utils/slaac_address.cpp
+++ b/src/core/utils/slaac_address.cpp
@@ -136,7 +136,7 @@
 }
 
 bool Slaac::DoesConfigMatchNetifAddr(const NetworkData::OnMeshPrefixConfig &aConfig,
-                                     const Ip6::Netif::UnicastAddress &     aAddr)
+                                     const Ip6::Netif::UnicastAddress      &aAddr)
 {
     return (((aConfig.mOnMesh && (aAddr.mPrefixLength == aConfig.mPrefix.mLength)) ||
              (!aConfig.mOnMesh && (aAddr.mPrefixLength == 128))) &&
@@ -204,7 +204,8 @@
         {
             Ip6::Prefix &prefix = config.GetPrefix();
 
-            if (config.mDp || !config.mSlaac || ShouldFilter(prefix))
+            if (config.mDp || !config.mSlaac || (prefix.GetLength() != Ip6::NetworkPrefix::kLength) ||
+                ShouldFilter(prefix))
             {
                 continue;
             }
@@ -255,9 +256,9 @@
 }
 
 Error Slaac::GenerateIid(Ip6::Netif::UnicastAddress &aAddress,
-                         uint8_t *                   aNetworkId,
+                         uint8_t                    *aNetworkId,
                          uint8_t                     aNetworkIdLength,
-                         uint8_t *                   aDadCounter) const
+                         uint8_t                    *aDadCounter) const
 {
     /*
      *  This method generates a semantically opaque IID per RFC 7217.
diff --git a/src/core/utils/slaac_address.hpp b/src/core/utils/slaac_address.hpp
index db7913b..1784d33 100644
--- a/src/core/utils/slaac_address.hpp
+++ b/src/core/utils/slaac_address.hpp
@@ -132,16 +132,16 @@
      * @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.
      *
      */
     Error GenerateIid(Ip6::Netif::UnicastAddress &aAddress,
-                      uint8_t *                   aNetworkId       = nullptr,
+                      uint8_t                    *aNetworkId       = nullptr,
                       uint8_t                     aNetworkIdLength = 0,
-                      uint8_t *                   aDadCounter      = nullptr) const;
+                      uint8_t                    *aDadCounter      = nullptr) const;
 
 private:
     static constexpr uint16_t kMaxIidCreationAttempts = 256; // Maximum number of attempts when generating IID.
@@ -163,7 +163,7 @@
     void        GetIidSecretKey(IidSecretKey &aKey) const;
     void        HandleNotifierEvents(Events aEvents);
     static bool DoesConfigMatchNetifAddr(const NetworkData::OnMeshPrefixConfig &aConfig,
-                                         const Ip6::Netif::UnicastAddress &     aAddr);
+                                         const Ip6::Netif::UnicastAddress      &aAddr);
 
     bool                       mEnabled;
     otIp6SlaacPrefixFilter     mFilter;
diff --git a/src/core/utils/srp_client_buffers.hpp b/src/core/utils/srp_client_buffers.hpp
index 7746de8..a60be7b 100644
--- a/src/core/utils/srp_client_buffers.hpp
+++ b/src/core/utils/srp_client_buffers.hpp
@@ -171,7 +171,7 @@
         }
 
     private:
-        ServiceEntry *      GetNext(void) { return reinterpret_cast<ServiceEntry *>(mService.mNext); }
+        ServiceEntry       *GetNext(void) { return reinterpret_cast<ServiceEntry *>(mService.mNext); }
         const ServiceEntry *GetNext(void) const { return reinterpret_cast<const ServiceEntry *>(mService.mNext); }
         void SetNext(ServiceEntry *aEntry) { mService.mNext = reinterpret_cast<Srp::Client::Service *>(aEntry); }
 
diff --git a/src/lib/hdlc/hdlc.hpp b/src/lib/hdlc/hdlc.hpp
index 84581d1..eb80f6f 100644
--- a/src/lib/hdlc/hdlc.hpp
+++ b/src/lib/hdlc/hdlc.hpp
@@ -584,7 +584,7 @@
     State              mState;
     FrameWritePointer &mWritePointer;
     FrameHandler       mFrameHandler;
-    void *             mContext;
+    void              *mContext;
     uint16_t           mFcs;
     uint16_t           mDecodedLength;
 };
diff --git a/src/lib/spinel/CMakeLists.txt b/src/lib/spinel/CMakeLists.txt
index 85114f7..0e41c87 100644
--- a/src/lib/spinel/CMakeLists.txt
+++ b/src/lib/spinel/CMakeLists.txt
@@ -31,13 +31,19 @@
 
 target_compile_definitions(openthread-spinel-ncp PRIVATE
     OPENTHREAD_FTD=1
-    OPENTHREAD_CONFIG_NCP_HDLC_ENABLE=1
     PUBLIC OPENTHREAD_SPINEL_CONFIG_OPENTHREAD_MESSAGE_ENABLE=1
 )
 
+if (OT_NCP_SPI)
+    target_compile_definitions(openthread-spinel-ncp PRIVATE OPENTHREAD_CONFIG_NCP_HDLC_ENABLE=0)
+    target_compile_definitions(openthread-spinel-rcp PRIVATE OPENTHREAD_CONFIG_NCP_HDLC_ENABLE=0)
+else()
+    target_compile_definitions(openthread-spinel-ncp PRIVATE OPENTHREAD_CONFIG_NCP_HDLC_ENABLE=1)
+    target_compile_definitions(openthread-spinel-rcp PRIVATE OPENTHREAD_CONFIG_NCP_HDLC_ENABLE=1)
+endif()
+
 target_compile_definitions(openthread-spinel-rcp PRIVATE
     OPENTHREAD_RADIO=1
-    OPENTHREAD_CONFIG_NCP_HDLC_ENABLE=1
     PUBLIC OPENTHREAD_SPINEL_CONFIG_OPENTHREAD_MESSAGE_ENABLE=0
 )
 
@@ -67,5 +73,24 @@
 target_sources(openthread-spinel-ncp PRIVATE ${COMMON_SOURCES})
 target_sources(openthread-spinel-rcp PRIVATE ${COMMON_SOURCES})
 
-target_link_libraries(openthread-spinel-ncp PRIVATE ot-config)
-target_link_libraries(openthread-spinel-rcp PRIVATE ot-config)
+target_link_libraries(openthread-spinel-ncp
+    PRIVATE
+        ot-config-ftd
+        ot-config
+)
+
+target_link_libraries(openthread-spinel-rcp
+    PRIVATE
+        ot-config-radio
+        ot-config
+)
+
+if(BUILD_TESTING)
+    add_executable(ot-test-spinel
+        spinel.c
+    )
+    target_compile_definitions(ot-test-spinel
+        PRIVATE -DSPINEL_SELF_TEST=1 -D_GNU_SOURCE
+    )
+    add_test(NAME ot-test-spinel COMMAND ot-test-spinel)
+endif()
diff --git a/src/lib/spinel/Makefile.am b/src/lib/spinel/Makefile.am
index 04061f6..1540424 100644
--- a/src/lib/spinel/Makefile.am
+++ b/src/lib/spinel/Makefile.am
@@ -99,18 +99,6 @@
 
 if OPENTHREAD_BUILD_TESTS
 
-check_PROGRAMS                                    = spinel-test
-spinel_test_SOURCES                               = spinel.c
-spinel_test_CFLAGS                                = \
-    $(COMMON_CPPFLAGS)                              \
-    -DSPINEL_SELF_TEST=1                            \
-    -D_GNU_SOURCE                                   \
-    -I$(top_srcdir)/src/core                        \
-    -I$(top_srcdir)/include                         \
-    $(NULL)
-
-TESTS                                             = spinel-test
-
 install-headers: install-includeHEADERS
 
 if OPENTHREAD_BUILD_COVERAGE
diff --git a/src/lib/spinel/openthread-spinel-config.h b/src/lib/spinel/openthread-spinel-config.h
index 3cd91f2..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
  *
@@ -55,4 +57,13 @@
 #define OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT 0
 #endif
 
+/**
+ * @def OPENTHREAD_SPINEL_CONFIG_ABORT_ON_UNEXPECTED_RCP_RESET_ENABLE
+ *
+ * Define 1 to abort the host when receiving unexpected reset from RCP.
+ *
+ */
+#ifndef OPENTHREAD_SPINEL_CONFIG_ABORT_ON_UNEXPECTED_RCP_RESET_ENABLE
+#define OPENTHREAD_SPINEL_CONFIG_ABORT_ON_UNEXPECTED_RCP_RESET_ENABLE 0
+#endif
 #endif // OPENTHREAD_SPINEL_CONFIG_H_
diff --git a/src/lib/spinel/radio_spinel.hpp b/src/lib/spinel/radio_spinel.hpp
index b0d3b46..332eff4 100644
--- a/src/lib/spinel/radio_spinel.hpp
+++ b/src/lib/spinel/radio_spinel.hpp
@@ -91,10 +91,9 @@
  *
  *    // This method performs radio driver processing.
  *
- *    // @param[in]   aContext        The context containing fd_sets.
- *    //                              The type is specified by the user in template parameters.
+ *    // @param[in]   aContext  The process context.
  *
- *    void Process(const ProcessContextType &aContext);
+ *    void Process(const void *aContext);
  *
  *
  *    // This method deinitializes the interface to the RCP.
@@ -102,7 +101,7 @@
  *    void Deinit(void);
  * };
  */
-template <typename InterfaceType, typename ProcessContextType> class RadioSpinel
+template <typename InterfaceType> class RadioSpinel
 {
 public:
     /**
@@ -562,7 +561,7 @@
      * @param[in]  aContext   The process context.
      *
      */
-    void Process(const ProcessContextType &aContext);
+    void Process(const void *aContext);
 
     /**
      * This method returns the underlying spinel interface.
@@ -648,10 +647,12 @@
     /**
      * This method sets the current MAC Frame Counter value.
      *
-     * @param[in]   aMacFrameCounter  The MAC Frame Counter value.
+     * @param[in] aMacFrameCounter  The MAC Frame Counter value.
+     * @param[in] aSetIfLarger      If `true`, set only if the new value is larger than the current value.
+     *                              If `false`, set the new value independent of the current value.
      *
      */
-    otError SetMacFrameCounter(uint32_t aMacFrameCounter);
+    otError SetMacFrameCounter(uint32_t aMacFrameCounter, bool aSetIfLarger);
 
     /**
      * This method sets the radio region code.
@@ -699,7 +700,7 @@
      */
     otError ConfigureEnhAckProbing(otLinkMetrics        aLinkMetrics,
                                    const otShortAddress aShortAddress,
-                                   const otExtAddress & aExtAddress);
+                                   const otExtAddress  &aExtAddress);
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
@@ -727,14 +728,16 @@
     /**
      * This method checks whether the spinel interface is radio-only.
      *
-     * @param[out] aSupportsRcpApiVersion   A reference to a boolean variable to update whether the list of spinel
-     *                                      capabilities include `SPINEL_CAP_RCP_API_VERSION`.
+     * @param[out] aSupportsRcpApiVersion          A reference to a boolean variable to update whether the list of
+     *                                             spinel capabilities includes `SPINEL_CAP_RCP_API_VERSION`.
+     * @param[out] aSupportsRcpMinHostApiVersion   A reference to a boolean variable to update whether the list of
+     *                                             spinel capabilities includes `SPINEL_CAP_RCP_MIN_HOST_API_VERSION`.
      *
      * @retval  TRUE    The radio chip is in radio-only mode.
      * @retval  FALSE   Otherwise.
      *
      */
-    bool IsRcp(bool &aSupportsRcpApiVersion);
+    bool IsRcp(bool &aSupportsRcpApiVersion, bool &aSupportsRcpMinHostApiVersion);
 
     /**
      * This method checks whether there is pending frame in the buffer.
@@ -820,9 +823,9 @@
      *
      */
     otError GetWithParam(spinel_prop_key_t aKey,
-                         const uint8_t *   aParam,
+                         const uint8_t    *aParam,
                          spinel_size_t     aParamSize,
-                         const char *      aFormat,
+                         const char       *aFormat,
                          ...);
 
     /**
@@ -886,6 +889,55 @@
      */
     const otRadioSpinelMetrics *GetRadioSpinelMetrics(void) const { return &mRadioSpinelMetrics; }
 
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+    /**
+     * 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.
+     * @param[in] aRawPowerSetting        A pointer to the raw power setting byte array.
+     * @param[in] aRawPowerSettingLength  The length of the @p aRawPowerSetting.
+     *
+     * @retval  OT_ERROR_NONE              Successfully added the calibrated power to the power calibration table.
+     * @retval  OT_ERROR_NO_BUFS           No available entry in the power calibration table.
+     * @retval  OT_ERROR_INVALID_ARGS      The @p aChannel, @p aActualPower or @p aRawPowerSetting is invalid.
+     * @retval  OT_ERROR_NOT_IMPLEMENTED   This feature is not implemented.
+     * @retval  OT_ERROR_BUSY              Failed due to another operation is on going.
+     * @retval  OT_ERROR_RESPONSE_TIMEOUT  Failed due to no response received from the transceiver.
+     *
+     */
+    otError AddCalibratedPower(uint8_t        aChannel,
+                               int16_t        aActualPower,
+                               const uint8_t *aRawPowerSetting,
+                               uint16_t       aRawPowerSettingLength);
+
+    /**
+     * Clear all calibrated powers from the power calibration table.
+     *
+     * @retval  OT_ERROR_NONE              Successfully cleared all calibrated powers from the power calibration table.
+     * @retval  OT_ERROR_NOT_IMPLEMENTED   This feature is not implemented.
+     * @retval  OT_ERROR_BUSY              Failed due to another operation is on going.
+     * @retval  OT_ERROR_RESPONSE_TIMEOUT  Failed due to no response received from the transceiver.
+     *
+     */
+    otError ClearCalibratedPowers(void);
+
+    /**
+     * Set the target power for the given channel.
+     *
+     * @param[in]  aChannel      The radio channel.
+     * @param[in]  aTargetPower  The target power in 0.01dBm. Passing `INT16_MAX` will disable this channel.
+     *
+     * @retval  OT_ERROR_NONE              Successfully set the target power.
+     * @retval  OT_ERROR_INVALID_ARGS      The @p aChannel or @p aTargetPower is invalid..
+     * @retval  OT_ERROR_NOT_IMPLEMENTED   The feature is not implemented.
+     * @retval  OT_ERROR_BUSY              Failed due to another operation is on going.
+     * @retval  OT_ERROR_RESPONSE_TIMEOUT  Failed due to no response received from the transceiver.
+     *
+     */
+    otError SetChannelTargetPower(uint8_t aChannel, int16_t aTargetPower);
+#endif
+
 private:
     enum
     {
@@ -909,9 +961,10 @@
 
     static void HandleReceivedFrame(void *aContext);
 
+    void    ResetRcp(bool aResetRadio);
     otError CheckSpinelVersion(void);
     otError CheckRadioCapabilities(void);
-    otError CheckRcpApiVersion(bool aSupportsRcpApiVersion);
+    otError CheckRcpApiVersion(bool aSupportsRcpApiVersion, bool aSupportsMinHostRcpApiVersion);
 
     /**
      * This method triggers a state transfer of the state machine.
@@ -930,26 +983,26 @@
 
     otError RequestV(uint32_t aCommand, spinel_prop_key_t aKey, const char *aFormat, va_list aArgs);
     otError Request(uint32_t aCommand, spinel_prop_key_t aKey, const char *aFormat, ...);
-    otError RequestWithPropertyFormat(const char *      aPropertyFormat,
+    otError RequestWithPropertyFormat(const char       *aPropertyFormat,
                                       uint32_t          aCommand,
                                       spinel_prop_key_t aKey,
-                                      const char *      aFormat,
+                                      const char       *aFormat,
                                       ...);
-    otError RequestWithPropertyFormatV(const char *      aPropertyFormat,
+    otError RequestWithPropertyFormatV(const char       *aPropertyFormat,
                                        uint32_t          aCommand,
                                        spinel_prop_key_t aKey,
-                                       const char *      aFormat,
+                                       const char       *aFormat,
                                        va_list           aArgs);
     otError RequestWithExpectedCommandV(uint32_t          aExpectedCommand,
                                         uint32_t          aCommand,
                                         spinel_prop_key_t aKey,
-                                        const char *      aFormat,
+                                        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,
-                        const char *      aFormat,
+                        const char       *aFormat,
                         va_list           aArgs);
     otError ParseRadioFrame(otRadioFrame &aFrame, const uint8_t *aBuffer, uint16_t aLength, spinel_ssize_t &aUnpacked);
     otError ThreadDatasetHandler(const uint8_t *aBuffer, uint16_t aLength);
@@ -996,6 +1049,9 @@
         mRadioSpinelMetrics.mSpinelParseErrorCount += (aError == OT_ERROR_PARSE) ? 1 : 0;
     }
 
+    uint32_t Snprintf(char *aDest, uint32_t aSize, const char *aFormat, ...);
+    void     LogSpinelFrame(const uint8_t *aFrame, uint16_t aLength, bool aTx);
+
     otInstance *mInstance;
 
     SpinelInterface::RxFrameBuffer mRxFrameBuffer;
@@ -1007,7 +1063,7 @@
     spinel_tid_t      mTxRadioTid;      ///< The transaction id used to send a radio frame.
     spinel_tid_t      mWaitingTid;      ///< The transaction id of current transaction.
     spinel_prop_key_t mWaitingKey;      ///< The property key of current transaction.
-    const char *      mPropertyFormat;  ///< The spinel property format of current transaction.
+    const char       *mPropertyFormat;  ///< The spinel property format of current transaction.
     va_list           mPropertyArgs;    ///< The arguments pack or unpack spinel property of current transaction.
     uint32_t          mExpectedCommand; ///< Expected response command of current transaction.
     otError           mError;           ///< The result of current transaction.
@@ -1070,13 +1126,13 @@
 
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
     bool   mDiagMode;
-    char * mDiagOutput;
+    char  *mDiagOutput;
     size_t mDiagOutputMaxLen;
 #endif
 
     uint64_t mTxRadioEndUs;
     uint64_t mRadioTimeRecalcStart; ///< When to recalculate RCP time offset.
-    int64_t  mRadioTimeOffset;      ///< Time difference with estimated RCP time minus host time.
+    uint64_t mRadioTimeOffset;      ///< Time difference with estimated RCP time minus host time.
 
     MaxPowerTable mMaxPowerTable;
 
diff --git a/src/lib/spinel/radio_spinel_impl.hpp b/src/lib/spinel/radio_spinel_impl.hpp
index c9eda5c..d4928c8 100644
--- a/src/lib/spinel/radio_spinel_impl.hpp
+++ b/src/lib/spinel/radio_spinel_impl.hpp
@@ -64,18 +64,11 @@
 #ifndef US_PER_S
 #define US_PER_S (MS_PER_S * US_PER_MS)
 #endif
-#ifndef NS_PER_US
-#define NS_PER_US 1000
-#endif
 
 #ifndef TX_WAIT_US
 #define TX_WAIT_US (5 * US_PER_S)
 #endif
 
-#ifndef RCP_TIME_OFFSET_CHECK_INTERVAL
-#define RCP_TIME_OFFSET_CHECK_INTERVAL (60 * US_PER_S)
-#endif
-
 using ot::Spinel::Decoder;
 
 namespace ot {
@@ -136,6 +129,9 @@
         break;
 
     case SPINEL_STATUS_PROP_NOT_FOUND:
+        ret = OT_ERROR_NOT_IMPLEMENTED;
+        break;
+
     case SPINEL_STATUS_ITEM_NOT_FOUND:
         ret = OT_ERROR_NOT_FOUND;
         break;
@@ -166,14 +162,13 @@
     }
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::HandleReceivedFrame(void *aContext)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::HandleReceivedFrame(void *aContext)
 {
     static_cast<RadioSpinel *>(aContext)->HandleReceivedFrame();
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-RadioSpinel<InterfaceType, ProcessContextType>::RadioSpinel(void)
+template <typename InterfaceType>
+RadioSpinel<InterfaceType>::RadioSpinel(void)
     : mInstance(nullptr)
     , mRxFrameBuffer()
     , mSpinelInterface(HandleReceivedFrame, this, mRxFrameBuffer)
@@ -215,46 +210,29 @@
 #endif
     , mTxRadioEndUs(UINT64_MAX)
     , mRadioTimeRecalcStart(UINT64_MAX)
-    , mRadioTimeOffset(0)
+    , mRadioTimeOffset(UINT64_MAX)
 {
     mVersion[0] = '\0';
     memset(&mRadioSpinelMetrics, 0, sizeof(mRadioSpinelMetrics));
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::Init(bool aResetRadio,
-                                                          bool aRestoreDatasetFromNcp,
-                                                          bool aSkipRcpCompatibilityCheck)
+template <typename InterfaceType>
+void RadioSpinel<InterfaceType>::Init(bool aResetRadio, bool aRestoreDatasetFromNcp, bool aSkipRcpCompatibilityCheck)
 {
     otError error = OT_ERROR_NONE;
     bool    supportsRcpApiVersion;
+    bool    supportsRcpMinHostApiVersion;
 
 #if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
     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));
 
-    if (!IsRcp(supportsRcpApiVersion))
+    if (!IsRcp(supportsRcpApiVersion, supportsRcpMinHostApiVersion))
     {
         uint8_t exitCode = OT_EXIT_RADIO_SPINEL_INCOMPATIBLE;
 
@@ -270,7 +248,7 @@
 
     if (!aSkipRcpCompatibilityCheck)
     {
-        SuccessOrDie(CheckRcpApiVersion(supportsRcpApiVersion));
+        SuccessOrDie(CheckRcpApiVersion(supportsRcpApiVersion, supportsRcpMinHostApiVersion));
         SuccessOrDie(CheckRadioCapabilities());
     }
 
@@ -282,8 +260,43 @@
     SuccessOrDie(error);
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::CheckSpinelVersion(void)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::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> otError RadioSpinel<InterfaceType>::CheckSpinelVersion(void)
 {
     otError      error = OT_ERROR_NONE;
     unsigned int versionMajor;
@@ -305,8 +318,8 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-bool RadioSpinel<InterfaceType, ProcessContextType>::IsRcp(bool &aSupportsRcpApiVersion)
+template <typename InterfaceType>
+bool RadioSpinel<InterfaceType>::IsRcp(bool &aSupportsRcpApiVersion, bool &aSupportsRcpMinHostApiVersion)
 {
     uint8_t        capsBuffer[kCapsBufferSize];
     const uint8_t *capsData         = capsBuffer;
@@ -314,7 +327,8 @@
     bool           supportsRawRadio = false;
     bool           isRcp            = false;
 
-    aSupportsRcpApiVersion = false;
+    aSupportsRcpApiVersion        = false;
+    aSupportsRcpMinHostApiVersion = false;
 
     SuccessOrDie(Get(SPINEL_PROP_CAPS, SPINEL_DATATYPE_DATA_S, capsBuffer, &capsLength));
 
@@ -346,6 +360,11 @@
             aSupportsRcpApiVersion = true;
         }
 
+        if (capability == SPINEL_PROP_RCP_MIN_HOST_API_VERSION)
+        {
+            aSupportsRcpMinHostApiVersion = true;
+        }
+
         capsData += unpacked;
         capsLength -= static_cast<spinel_size_t>(unpacked);
     }
@@ -359,8 +378,7 @@
     return isRcp;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::CheckRadioCapabilities(void)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::CheckRadioCapabilities(void)
 {
     const otRadioCaps kRequiredRadioCaps =
 #if OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2
@@ -396,30 +414,50 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::CheckRcpApiVersion(bool aSupportsRcpApiVersion)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::CheckRcpApiVersion(bool aSupportsRcpApiVersion, bool aSupportsRcpMinHostApiVersion)
 {
-    otError      error         = OT_ERROR_NONE;
-    unsigned int rcpApiVersion = 1;
-
-    // Use RCP API Version value 1, when the RCP capability
-    // list does not contain `SPINEL_CAP_RCP_API_VERSION`.
-
-    if (aSupportsRcpApiVersion)
-    {
-        SuccessOrExit(error = Get(SPINEL_PROP_RCP_API_VERSION, SPINEL_DATATYPE_UINT_PACKED_S, &rcpApiVersion));
-    }
-
-    otLogNotePlat("RCP API Version: %u", rcpApiVersion);
+    otError error = OT_ERROR_NONE;
 
     static_assert(SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION <= SPINEL_RCP_API_VERSION,
                   "MIN_HOST_SUPPORTED_RCP_API_VERSION must be smaller than or equal to RCP_API_VERSION");
 
-    if ((rcpApiVersion < SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION) || (rcpApiVersion > SPINEL_RCP_API_VERSION))
+    if (aSupportsRcpApiVersion)
     {
-        otLogCritPlat("RCP API Version %u is not in the supported range [%u-%u]", rcpApiVersion,
-                      SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION, SPINEL_RCP_API_VERSION);
-        DieNow(OT_EXIT_RADIO_SPINEL_INCOMPATIBLE);
+        // Make sure RCP is not too old and its version is within the
+        // range host supports.
+
+        unsigned int rcpApiVersion;
+
+        SuccessOrExit(error = Get(SPINEL_PROP_RCP_API_VERSION, SPINEL_DATATYPE_UINT_PACKED_S, &rcpApiVersion));
+
+        if (rcpApiVersion < SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION)
+        {
+            otLogCritPlat("RCP and host are using incompatible API versions");
+            otLogCritPlat("RCP API Version %u is older than min required by host %u", rcpApiVersion,
+                          SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION);
+            DieNow(OT_EXIT_RADIO_SPINEL_INCOMPATIBLE);
+        }
+    }
+
+    if (aSupportsRcpMinHostApiVersion)
+    {
+        // Check with RCP about min host API version it can work with,
+        // and make sure on host side our version is within the supported
+        // range.
+
+        unsigned int minHostRcpApiVersion;
+
+        SuccessOrExit(
+            error = Get(SPINEL_PROP_RCP_MIN_HOST_API_VERSION, SPINEL_DATATYPE_UINT_PACKED_S, &minHostRcpApiVersion));
+
+        if (SPINEL_RCP_API_VERSION < minHostRcpApiVersion)
+        {
+            otLogCritPlat("RCP and host are using incompatible API versions");
+            otLogCritPlat("RCP requires min host API version %u but host is older and at version %u",
+                          minHostRcpApiVersion, SPINEL_RCP_API_VERSION);
+            DieNow(OT_EXIT_RADIO_SPINEL_INCOMPATIBLE);
+        }
     }
 
 exit:
@@ -427,8 +465,7 @@
 }
 
 #if !OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::RestoreDatasetFromNcp(void)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::RestoreDatasetFromNcp(void)
 {
     otError error = OT_ERROR_NONE;
 
@@ -446,21 +483,20 @@
 }
 #endif
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::Deinit(void)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::Deinit(void)
 {
     mSpinelInterface.Deinit();
     // This allows implementing pseudo reset.
     new (this) RadioSpinel();
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::HandleReceivedFrame(void)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::HandleReceivedFrame(void)
 {
     otError        error = OT_ERROR_NONE;
     uint8_t        header;
     spinel_ssize_t unpacked;
 
+    LogSpinelFrame(mRxFrameBuffer.GetFrame(), mRxFrameBuffer.GetLength(), false);
     unpacked = spinel_datatype_unpack(mRxFrameBuffer.GetFrame(), mRxFrameBuffer.GetLength(), "C", &header);
 
     VerifyOrExit(unpacked > 0 && (header & SPINEL_HEADER_FLAG) == SPINEL_HEADER_FLAG &&
@@ -487,13 +523,13 @@
     UpdateParseErrorCount(error);
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::HandleNotification(SpinelInterface::RxFrameBuffer &aFrameBuffer)
+template <typename InterfaceType>
+void RadioSpinel<InterfaceType>::HandleNotification(SpinelInterface::RxFrameBuffer &aFrameBuffer)
 {
     spinel_prop_key_t key;
     spinel_size_t     len = 0;
     spinel_ssize_t    unpacked;
-    uint8_t *         data = nullptr;
+    uint8_t          *data = nullptr;
     uint32_t          cmd;
     uint8_t           header;
     otError           error           = OT_ERROR_NONE;
@@ -521,7 +557,7 @@
 
     case SPINEL_CMD_PROP_VALUE_INSERTED:
     case SPINEL_CMD_PROP_VALUE_REMOVED:
-        otLogInfoPlat("Ignored command %d", cmd);
+        otLogInfoPlat("Ignored command %lu", ToUlong(cmd));
         break;
 
     default:
@@ -542,13 +578,13 @@
     LogIfFail("Error processing notification", error);
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::HandleNotification(const uint8_t *aFrame, uint16_t aLength)
+template <typename InterfaceType>
+void RadioSpinel<InterfaceType>::HandleNotification(const uint8_t *aFrame, uint16_t aLength)
 {
     spinel_prop_key_t key;
     spinel_size_t     len = 0;
     spinel_ssize_t    unpacked;
-    uint8_t *         data = nullptr;
+    uint8_t          *data = nullptr;
     uint32_t          cmd;
     uint8_t           header;
     otError           error = OT_ERROR_NONE;
@@ -564,11 +600,11 @@
     LogIfFail("Error processing saved notification", error);
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::HandleResponse(const uint8_t *aBuffer, uint16_t aLength)
+template <typename InterfaceType>
+void RadioSpinel<InterfaceType>::HandleResponse(const uint8_t *aBuffer, uint16_t aLength)
 {
     spinel_prop_key_t key;
-    uint8_t *         data   = nullptr;
+    uint8_t          *data   = nullptr;
     spinel_size_t     len    = 0;
     uint8_t           header = 0;
     uint32_t          cmd    = 0;
@@ -607,8 +643,8 @@
 }
 
 #if !OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::ThreadDatasetHandler(const uint8_t *aBuffer, uint16_t aLength)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::ThreadDatasetHandler(const uint8_t *aBuffer, uint16_t aLength)
 {
     otError              error = OT_ERROR_NONE;
     otOperationalDataset opDataset;
@@ -771,11 +807,11 @@
 }
 #endif // #if !OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::HandleWaitingResponse(uint32_t          aCommand,
-                                                                           spinel_prop_key_t aKey,
-                                                                           const uint8_t *   aBuffer,
-                                                                           uint16_t          aLength)
+template <typename InterfaceType>
+void RadioSpinel<InterfaceType>::HandleWaitingResponse(uint32_t          aCommand,
+                                                       spinel_prop_key_t aKey,
+                                                       const uint8_t    *aBuffer,
+                                                       uint16_t          aLength)
 {
     if (aKey == SPINEL_PROP_LAST_STATUS)
     {
@@ -790,6 +826,7 @@
     {
         spinel_ssize_t unpacked;
 
+        mError = OT_ERROR_NONE;
         VerifyOrExit(mDiagOutput != nullptr);
         unpacked =
             spinel_datatype_unpack_in_place(aBuffer, aLength, SPINEL_DATATYPE_UTF8_S, mDiagOutput, &mDiagOutputMaxLen);
@@ -839,10 +876,8 @@
     LogIfFail("Error processing result", mError);
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::HandleValueIs(spinel_prop_key_t aKey,
-                                                                   const uint8_t *   aBuffer,
-                                                                   uint16_t          aLength)
+template <typename InterfaceType>
+void RadioSpinel<InterfaceType>::HandleValueIs(spinel_prop_key_t aKey, const uint8_t *aBuffer, uint16_t aLength)
 {
     otError        error = OT_ERROR_NONE;
     spinel_ssize_t unpacked;
@@ -947,11 +982,11 @@
     LogIfFail("Failed to handle ValueIs", error);
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::ParseRadioFrame(otRadioFrame &  aFrame,
-                                                                        const uint8_t * aBuffer,
-                                                                        uint16_t        aLength,
-                                                                        spinel_ssize_t &aUnpacked)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::ParseRadioFrame(otRadioFrame   &aFrame,
+                                                    const uint8_t  *aBuffer,
+                                                    uint16_t        aLength,
+                                                    spinel_ssize_t &aUnpacked)
 {
     otError        error        = OT_ERROR_NONE;
     uint16_t       flags        = 0;
@@ -1020,8 +1055,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::ProcessFrameQueue(void)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::ProcessFrameQueue(void)
 {
     uint8_t *frame = nullptr;
     uint16_t length;
@@ -1034,8 +1068,7 @@
     mRxFrameBuffer.ClearSavedFrames();
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::RadioReceive(void)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::RadioReceive(void)
 {
     if (!mIsPromiscuous)
     {
@@ -1067,10 +1100,8 @@
     return;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::TransmitDone(otRadioFrame *aFrame,
-                                                                  otRadioFrame *aAckFrame,
-                                                                  otError       aError)
+template <typename InterfaceType>
+void RadioSpinel<InterfaceType>::TransmitDone(otRadioFrame *aFrame, otRadioFrame *aAckFrame, otError aError)
 {
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
     if (otPlatDiagModeGet())
@@ -1084,8 +1115,7 @@
     }
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::ProcessRadioStateMachine(void)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::ProcessRadioStateMachine(void)
 {
     if (mState == kStateTransmitDone)
     {
@@ -1102,8 +1132,7 @@
     }
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::Process(const ProcessContextType &aContext)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::Process(const void *aContext)
 {
     if (mRxFrameBuffer.HasSavedFrame())
     {
@@ -1125,8 +1154,7 @@
     CalcRcpTimeOffset();
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetPromiscuous(bool aEnable)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::SetPromiscuous(bool aEnable)
 {
     otError error;
 
@@ -1138,8 +1166,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetShortAddress(uint16_t aAddress)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::SetShortAddress(uint16_t aAddress)
 {
     otError error = OT_ERROR_NONE;
 
@@ -1151,12 +1178,12 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetMacKey(uint8_t                 aKeyIdMode,
-                                                                  uint8_t                 aKeyId,
-                                                                  const otMacKeyMaterial *aPrevKey,
-                                                                  const otMacKeyMaterial *aCurrKey,
-                                                                  const otMacKeyMaterial *aNextKey)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::SetMacKey(uint8_t                 aKeyIdMode,
+                                              uint8_t                 aKeyId,
+                                              const otMacKeyMaterial *aPrevKey,
+                                              const otMacKeyMaterial *aCurrKey,
+                                              const otMacKeyMaterial *aNextKey)
 {
     otError error;
     size_t  aKeySize;
@@ -1194,27 +1221,27 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetMacFrameCounter(uint32_t aMacFrameCounter)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::SetMacFrameCounter(uint32_t aMacFrameCounter, bool aSetIfLarger)
 {
     otError error;
 
-    SuccessOrExit(error = Set(SPINEL_PROP_RCP_MAC_FRAME_COUNTER, SPINEL_DATATYPE_UINT32_S, aMacFrameCounter));
+    SuccessOrExit(error = Set(SPINEL_PROP_RCP_MAC_FRAME_COUNTER, SPINEL_DATATYPE_UINT32_S SPINEL_DATATYPE_BOOL_S,
+                              aMacFrameCounter, aSetIfLarger));
 
 exit:
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::GetIeeeEui64(uint8_t *aIeeeEui64)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::GetIeeeEui64(uint8_t *aIeeeEui64)
 {
     memcpy(aIeeeEui64, mIeeeEui64.m8, sizeof(mIeeeEui64.m8));
 
     return OT_ERROR_NONE;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetExtendedAddress(const otExtAddress &aExtAddress)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::SetExtendedAddress(const otExtAddress &aExtAddress)
 {
     otError error;
 
@@ -1225,8 +1252,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetPanId(uint16_t aPanId)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::SetPanId(uint16_t aPanId)
 {
     otError error = OT_ERROR_NONE;
 
@@ -1238,14 +1264,12 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::EnableSrcMatch(bool aEnable)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::EnableSrcMatch(bool aEnable)
 {
     return Set(SPINEL_PROP_MAC_SRC_MATCH_ENABLED, SPINEL_DATATYPE_BOOL_S, aEnable);
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::AddSrcMatchShortEntry(uint16_t aShortAddress)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::AddSrcMatchShortEntry(uint16_t aShortAddress)
 {
     otError error;
 
@@ -1269,8 +1293,8 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::AddSrcMatchExtEntry(const otExtAddress &aExtAddress)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::AddSrcMatchExtEntry(const otExtAddress &aExtAddress)
 {
     otError error;
 
@@ -1295,8 +1319,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::ClearSrcMatchShortEntry(uint16_t aShortAddress)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::ClearSrcMatchShortEntry(uint16_t aShortAddress)
 {
     otError error;
 
@@ -1318,8 +1341,8 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::ClearSrcMatchExtEntry(const otExtAddress &aExtAddress)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::ClearSrcMatchExtEntry(const otExtAddress &aExtAddress)
 {
     otError error;
 
@@ -1342,8 +1365,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::ClearSrcMatchShortEntries(void)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::ClearSrcMatchShortEntries(void)
 {
     otError error;
 
@@ -1357,8 +1379,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::ClearSrcMatchExtEntries(void)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::ClearSrcMatchExtEntries(void)
 {
     otError error;
 
@@ -1372,8 +1393,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::GetTransmitPower(int8_t &aPower)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::GetTransmitPower(int8_t &aPower)
 {
     otError error = Get(SPINEL_PROP_PHY_TX_POWER, SPINEL_DATATYPE_INT8_S, &aPower);
 
@@ -1381,8 +1401,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::GetCcaEnergyDetectThreshold(int8_t &aThreshold)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::GetCcaEnergyDetectThreshold(int8_t &aThreshold)
 {
     otError error = Get(SPINEL_PROP_PHY_CCA_THRESHOLD, SPINEL_DATATYPE_INT8_S, &aThreshold);
 
@@ -1390,8 +1409,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::GetFemLnaGain(int8_t &aGain)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::GetFemLnaGain(int8_t &aGain)
 {
     otError error = Get(SPINEL_PROP_PHY_FEM_LNA_GAIN, SPINEL_DATATYPE_INT8_S, &aGain);
 
@@ -1399,8 +1417,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-int8_t RadioSpinel<InterfaceType, ProcessContextType>::GetRssi(void)
+template <typename InterfaceType> int8_t RadioSpinel<InterfaceType>::GetRssi(void)
 {
     int8_t  rssi  = OT_RADIO_RSSI_INVALID;
     otError error = Get(SPINEL_PROP_PHY_RSSI, SPINEL_DATATYPE_INT8_S, &rssi);
@@ -1410,8 +1427,7 @@
 }
 
 #if OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetCoexEnabled(bool aEnabled)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::SetCoexEnabled(bool aEnabled)
 {
     otError error;
 
@@ -1426,8 +1442,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-bool RadioSpinel<InterfaceType, ProcessContextType>::IsCoexEnabled(void)
+template <typename InterfaceType> bool RadioSpinel<InterfaceType>::IsCoexEnabled(void)
 {
     bool    enabled;
     otError error = Get(SPINEL_PROP_RADIO_COEX_ENABLE, SPINEL_DATATYPE_BOOL_S, &enabled);
@@ -1436,8 +1451,7 @@
     return enabled;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::GetCoexMetrics(otRadioCoexMetrics &aCoexMetrics)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::GetCoexMetrics(otRadioCoexMetrics &aCoexMetrics)
 {
     otError error;
 
@@ -1477,8 +1491,7 @@
 }
 #endif
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetTransmitPower(int8_t aPower)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::SetTransmitPower(int8_t aPower)
 {
     otError error;
 
@@ -1494,8 +1507,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetCcaEnergyDetectThreshold(int8_t aThreshold)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::SetCcaEnergyDetectThreshold(int8_t aThreshold)
 {
     otError error;
 
@@ -1511,8 +1523,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetFemLnaGain(int8_t aGain)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::SetFemLnaGain(int8_t aGain)
 {
     otError error;
 
@@ -1528,8 +1539,8 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::EnergyScan(uint8_t aScanChannel, uint16_t aScanDuration)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::EnergyScan(uint8_t aScanChannel, uint16_t aScanDuration)
 {
     otError error;
 
@@ -1551,8 +1562,8 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::Get(spinel_prop_key_t aKey, const char *aFormat, ...)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::Get(spinel_prop_key_t aKey, const char *aFormat, ...)
 {
     otError error;
 
@@ -1574,12 +1585,12 @@
 }
 
 // This is not a normal use case for VALUE_GET command and should be only used to get RCP timestamp with dummy payload
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::GetWithParam(spinel_prop_key_t aKey,
-                                                                     const uint8_t *   aParam,
-                                                                     spinel_size_t     aParamSize,
-                                                                     const char *      aFormat,
-                                                                     ...)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::GetWithParam(spinel_prop_key_t aKey,
+                                                 const uint8_t    *aParam,
+                                                 spinel_size_t     aParamSize,
+                                                 const char       *aFormat,
+                                                 ...)
 {
     otError error;
 
@@ -1601,8 +1612,8 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::Set(spinel_prop_key_t aKey, const char *aFormat, ...)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::Set(spinel_prop_key_t aKey, const char *aFormat, ...)
 {
     otError error;
 
@@ -1624,8 +1635,8 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::Insert(spinel_prop_key_t aKey, const char *aFormat, ...)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::Insert(spinel_prop_key_t aKey, const char *aFormat, ...)
 {
     otError error;
 
@@ -1647,8 +1658,8 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::Remove(spinel_prop_key_t aKey, const char *aFormat, ...)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::Remove(spinel_prop_key_t aKey, const char *aFormat, ...)
 {
     otError error;
 
@@ -1670,12 +1681,11 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::WaitResponse(void)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::WaitResponse(bool aHandleRcpTimeout)
 {
     uint64_t end = otPlatTimeGet() + kMaxWaitTime * US_PER_MS;
 
-    otLogDebgPlat("Wait response: tid=%u key=%u", mWaitingTid, mWaitingKey);
+    otLogDebgPlat("Wait response: tid=%u key=%lu", mWaitingTid, ToUlong(mWaitingKey));
 
     do
     {
@@ -1685,7 +1695,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);
@@ -1698,8 +1711,7 @@
     return mError;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-spinel_tid_t RadioSpinel<InterfaceType, ProcessContextType>::GetNextTid(void)
+template <typename InterfaceType> spinel_tid_t RadioSpinel<InterfaceType>::GetNextTid(void)
 {
     spinel_tid_t tid = mCmdNextTid;
 
@@ -1723,8 +1735,7 @@
     return tid;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SendReset(uint8_t aResetType)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::SendReset(uint8_t aResetType)
 {
     otError        error = OT_ERROR_NONE;
     uint8_t        buffer[kMaxSpinelFrame];
@@ -1737,17 +1748,18 @@
     VerifyOrExit(packed > 0 && static_cast<size_t>(packed) <= sizeof(buffer), error = OT_ERROR_NO_BUFS);
 
     SuccessOrExit(error = mSpinelInterface.SendFrame(buffer, static_cast<uint16_t>(packed)));
+    LogSpinelFrame(buffer, static_cast<uint16_t>(packed), true);
 
 exit:
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SendCommand(uint32_t          aCommand,
-                                                                    spinel_prop_key_t aKey,
-                                                                    spinel_tid_t      tid,
-                                                                    const char *      aFormat,
-                                                                    va_list           args)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::SendCommand(uint32_t          aCommand,
+                                                spinel_prop_key_t aKey,
+                                                spinel_tid_t      tid,
+                                                const char       *aFormat,
+                                                va_list           args)
 {
     otError        error = OT_ERROR_NONE;
     uint8_t        buffer[kMaxSpinelFrame];
@@ -1771,17 +1783,18 @@
         offset += static_cast<uint16_t>(packed);
     }
 
-    error = mSpinelInterface.SendFrame(buffer, offset);
+    SuccessOrExit(error = mSpinelInterface.SendFrame(buffer, offset));
+    LogSpinelFrame(buffer, offset, true);
 
 exit:
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::RequestV(uint32_t          command,
-                                                                 spinel_prop_key_t aKey,
-                                                                 const char *      aFormat,
-                                                                 va_list           aArgs)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::RequestV(uint32_t          command,
+                                             spinel_prop_key_t aKey,
+                                             const char       *aFormat,
+                                             va_list           aArgs)
 {
     otError      error = OT_ERROR_NONE;
     spinel_tid_t tid   = GetNextTid();
@@ -1809,11 +1822,8 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::Request(uint32_t          aCommand,
-                                                                spinel_prop_key_t aKey,
-                                                                const char *      aFormat,
-                                                                ...)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::Request(uint32_t aCommand, spinel_prop_key_t aKey, const char *aFormat, ...)
 {
     va_list args;
     va_start(args, aFormat);
@@ -1822,12 +1832,12 @@
     return status;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::RequestWithPropertyFormat(const char *      aPropertyFormat,
-                                                                                  uint32_t          aCommand,
-                                                                                  spinel_prop_key_t aKey,
-                                                                                  const char *      aFormat,
-                                                                                  ...)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::RequestWithPropertyFormat(const char       *aPropertyFormat,
+                                                              uint32_t          aCommand,
+                                                              spinel_prop_key_t aKey,
+                                                              const char       *aFormat,
+                                                              ...)
 {
     otError error;
     va_list args;
@@ -1839,12 +1849,12 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::RequestWithPropertyFormatV(const char *      aPropertyFormat,
-                                                                                   uint32_t          aCommand,
-                                                                                   spinel_prop_key_t aKey,
-                                                                                   const char *      aFormat,
-                                                                                   va_list           aArgs)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::RequestWithPropertyFormatV(const char       *aPropertyFormat,
+                                                               uint32_t          aCommand,
+                                                               spinel_prop_key_t aKey,
+                                                               const char       *aFormat,
+                                                               va_list           aArgs)
 {
     otError error;
 
@@ -1855,12 +1865,12 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::RequestWithExpectedCommandV(uint32_t          aExpectedCommand,
-                                                                                    uint32_t          aCommand,
-                                                                                    spinel_prop_key_t aKey,
-                                                                                    const char *      aFormat,
-                                                                                    va_list           aArgs)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::RequestWithExpectedCommandV(uint32_t          aExpectedCommand,
+                                                                uint32_t          aCommand,
+                                                                spinel_prop_key_t aKey,
+                                                                const char       *aFormat,
+                                                                va_list           aArgs)
 {
     otError error;
 
@@ -1871,11 +1881,11 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::HandleTransmitDone(uint32_t          aCommand,
-                                                                        spinel_prop_key_t aKey,
-                                                                        const uint8_t *   aBuffer,
-                                                                        uint16_t          aLength)
+template <typename InterfaceType>
+void RadioSpinel<InterfaceType>::HandleTransmitDone(uint32_t          aCommand,
+                                                    spinel_prop_key_t aKey,
+                                                    const uint8_t    *aBuffer,
+                                                    uint16_t          aLength)
 {
     otError         error         = OT_ERROR_NONE;
     spinel_status_t status        = SPINEL_STATUS_OK;
@@ -1937,8 +1947,7 @@
     LogIfFail("Handle transmit done failed", error);
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::Transmit(otRadioFrame &aFrame)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::Transmit(otRadioFrame &aFrame)
 {
     otError error = OT_ERROR_INVALID_STATE;
 
@@ -1950,21 +1959,23 @@
     otPlatRadioTxStarted(mInstance, mTransmitFrame);
 
     error = Request(SPINEL_CMD_PROP_VALUE_SET, SPINEL_PROP_STREAM_RAW,
-                    SPINEL_DATATYPE_DATA_WLEN_S                                   // Frame data
-                        SPINEL_DATATYPE_UINT8_S                                   // Channel
-                            SPINEL_DATATYPE_UINT8_S                               // MaxCsmaBackoffs
-                                SPINEL_DATATYPE_UINT8_S                           // MaxFrameRetries
-                                    SPINEL_DATATYPE_BOOL_S                        // CsmaCaEnabled
-                                        SPINEL_DATATYPE_BOOL_S                    // IsHeaderUpdated
-                                            SPINEL_DATATYPE_BOOL_S                // IsARetx
-                                                SPINEL_DATATYPE_BOOL_S            // SkipAes
-                                                    SPINEL_DATATYPE_UINT32_S      // TxDelay
-                                                        SPINEL_DATATYPE_UINT32_S, // TxDelayBaseTime
+                    SPINEL_DATATYPE_DATA_WLEN_S                                      // Frame data
+                        SPINEL_DATATYPE_UINT8_S                                      // Channel
+                            SPINEL_DATATYPE_UINT8_S                                  // MaxCsmaBackoffs
+                                SPINEL_DATATYPE_UINT8_S                              // MaxFrameRetries
+                                    SPINEL_DATATYPE_BOOL_S                           // CsmaCaEnabled
+                                        SPINEL_DATATYPE_BOOL_S                       // IsHeaderUpdated
+                                            SPINEL_DATATYPE_BOOL_S                   // IsARetx
+                                                SPINEL_DATATYPE_BOOL_S               // IsSecurityProcessed
+                                                    SPINEL_DATATYPE_UINT32_S         // TxDelay
+                                                        SPINEL_DATATYPE_UINT32_S     // TxDelayBaseTime
+                                                            SPINEL_DATATYPE_UINT8_S, // RxChannelAfterTxDone
                     mTransmitFrame->mPsdu, mTransmitFrame->mLength, mTransmitFrame->mChannel,
                     mTransmitFrame->mInfo.mTxInfo.mMaxCsmaBackoffs, mTransmitFrame->mInfo.mTxInfo.mMaxFrameRetries,
                     mTransmitFrame->mInfo.mTxInfo.mCsmaCaEnabled, mTransmitFrame->mInfo.mTxInfo.mIsHeaderUpdated,
                     mTransmitFrame->mInfo.mTxInfo.mIsARetx, mTransmitFrame->mInfo.mTxInfo.mIsSecurityProcessed,
-                    mTransmitFrame->mInfo.mTxInfo.mTxDelay, mTransmitFrame->mInfo.mTxInfo.mTxDelayBaseTime);
+                    mTransmitFrame->mInfo.mTxInfo.mTxDelay, mTransmitFrame->mInfo.mTxInfo.mTxDelayBaseTime,
+                    mTransmitFrame->mInfo.mTxInfo.mRxChannelAfterTxDone);
 
     if (error == OT_ERROR_NONE)
     {
@@ -1978,8 +1989,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::Receive(uint8_t aChannel)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::Receive(uint8_t aChannel)
 {
     otError error = OT_ERROR_NONE;
 
@@ -2010,8 +2020,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::Sleep(void)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::Sleep(void)
 {
     otError error = OT_ERROR_NONE;
 
@@ -2036,8 +2045,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::Enable(otInstance *aInstance)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::Enable(otInstance *aInstance)
 {
     otError error = OT_ERROR_NONE;
 
@@ -2062,8 +2070,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::Disable(void)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::Disable(void)
 {
     otError error = OT_ERROR_NONE;
 
@@ -2079,10 +2086,8 @@
 }
 
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::PlatDiagProcess(const char *aString,
-                                                                        char *      aOutput,
-                                                                        size_t      aOutputMaxLen)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::PlatDiagProcess(const char *aString, char *aOutput, size_t aOutputMaxLen)
 {
     otError error;
 
@@ -2098,8 +2103,7 @@
 }
 #endif
 
-template <typename InterfaceType, typename ProcessContextType>
-uint32_t RadioSpinel<InterfaceType, ProcessContextType>::GetRadioChannelMask(bool aPreferred)
+template <typename InterfaceType> uint32_t RadioSpinel<InterfaceType>::GetRadioChannelMask(bool aPreferred)
 {
     uint8_t        maskBuffer[kChannelMaskBufferSize];
     otError        error       = OT_ERROR_NONE;
@@ -2132,8 +2136,7 @@
     return channelMask;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otRadioState RadioSpinel<InterfaceType, ProcessContextType>::GetState(void) const
+template <typename InterfaceType> otRadioState RadioSpinel<InterfaceType>::GetState(void) const
 {
     static const otRadioState sOtRadioStateMap[] = {
         OT_RADIO_STATE_DISABLED, OT_RADIO_STATE_SLEEP,    OT_RADIO_STATE_RECEIVE,
@@ -2143,8 +2146,7 @@
     return sOtRadioStateMap[mState];
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::CalcRcpTimeOffset(void)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::CalcRcpTimeOffset(void)
 {
 #if OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2
     otError        error = OT_ERROR_NONE;
@@ -2196,29 +2198,26 @@
 
     VerifyOrExit(error == OT_ERROR_NONE, mRadioTimeRecalcStart = localRxTimestamp);
 
-    mRadioTimeOffset      = static_cast<int64_t>(remoteTimestamp - ((localRxTimestamp / 2) + (localTxTimestamp / 2)));
+    mRadioTimeOffset      = (remoteTimestamp - ((localRxTimestamp / 2) + (localTxTimestamp / 2)));
     mIsTimeSynced         = true;
-    mRadioTimeRecalcStart = localRxTimestamp + RCP_TIME_OFFSET_CHECK_INTERVAL;
+    mRadioTimeRecalcStart = localRxTimestamp + OPENTHREAD_POSIX_CONFIG_RCP_TIME_SYNC_INTERVAL;
 
 exit:
     LogIfFail("Error calculating RCP time offset: %s", error);
 #endif // OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-uint64_t RadioSpinel<InterfaceType, ProcessContextType>::GetNow(void)
+template <typename InterfaceType> uint64_t RadioSpinel<InterfaceType>::GetNow(void)
 {
-    return mIsTimeSynced ? (otPlatTimeGet() + static_cast<uint64_t>(mRadioTimeOffset)) : UINT64_MAX;
+    return (mIsTimeSynced) ? (otPlatTimeGet() + mRadioTimeOffset) : UINT64_MAX;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-uint32_t RadioSpinel<InterfaceType, ProcessContextType>::GetBusSpeed(void) const
+template <typename InterfaceType> uint32_t RadioSpinel<InterfaceType>::GetBusSpeed(void) const
 {
     return mSpinelInterface.GetBusSpeed();
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::HandleRcpUnexpectedReset(spinel_status_t aStatus)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::HandleRcpUnexpectedReset(spinel_status_t aStatus)
 {
     OT_UNUSED_VARIABLE(aStatus);
 
@@ -2227,13 +2226,14 @@
 
 #if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
     mRcpFailed = true;
+#elif OPENTHREAD_SPINEL_CONFIG_ABORT_ON_UNEXPECTED_RCP_RESET_ENABLE
+    abort();
 #else
     DieNow(OT_EXIT_RADIO_SPINEL_RESET);
 #endif
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::HandleRcpTimeout(void)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::HandleRcpTimeout(void)
 {
     mRadioSpinelMetrics.mRcpTimeoutCount++;
 
@@ -2244,8 +2244,7 @@
 #endif
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::RecoverFromRcpFailure(void)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::RecoverFromRcpFailure(void)
 {
 #if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
     constexpr int16_t kMaxFailureCount = OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT;
@@ -2271,24 +2270,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;
 
@@ -2325,8 +2314,7 @@
 }
 
 #if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
-template <typename InterfaceType, typename ProcessContextType>
-void RadioSpinel<InterfaceType, ProcessContextType>::RestoreProperties(void)
+template <typename InterfaceType> void RadioSpinel<InterfaceType>::RestoreProperties(void)
 {
     Settings::NetworkInfo networkInfo;
 
@@ -2383,6 +2371,7 @@
         SuccessOrDie(Set(SPINEL_PROP_PHY_FEM_LNA_GAIN, SPINEL_DATATYPE_INT8_S, mFemLnaGain));
     }
 
+#if OPENTHREAD_POSIX_CONFIG_MAX_POWER_TABLE_ENABLE
     for (uint8_t channel = Radio::kChannelMin; channel <= Radio::kChannelMax; channel++)
     {
         int8_t power = mMaxPowerTable.GetTransmitPower(channel);
@@ -2398,13 +2387,14 @@
             }
         }
     }
+#endif // OPENTHREAD_POSIX_CONFIG_MAX_POWER_TABLE_ENABLE
 
     CalcRcpTimeOffset();
 }
 #endif // OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetChannelMaxTransmitPower(uint8_t aChannel, int8_t aMaxPower)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::SetChannelMaxTransmitPower(uint8_t aChannel, int8_t aMaxPower)
 {
     otError error = OT_ERROR_NONE;
     VerifyOrExit(aChannel >= Radio::kChannelMin && aChannel <= Radio::kChannelMax, error = OT_ERROR_INVALID_ARGS);
@@ -2415,8 +2405,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::SetRadioRegion(uint16_t aRegionCode)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::SetRadioRegion(uint16_t aRegionCode)
 {
     otError error;
 
@@ -2436,8 +2425,7 @@
     return error;
 }
 
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::GetRadioRegion(uint16_t *aRegionCode)
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::GetRadioRegion(uint16_t *aRegionCode)
 {
     otError error = OT_ERROR_NONE;
 
@@ -2449,10 +2437,10 @@
 }
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::ConfigureEnhAckProbing(otLinkMetrics        aLinkMetrics,
-                                                                               const otShortAddress aShortAddress,
-                                                                               const otExtAddress & aExtAddress)
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::ConfigureEnhAckProbing(otLinkMetrics        aLinkMetrics,
+                                                           const otShortAddress aShortAddress,
+                                                           const otExtAddress  &aExtAddress)
 {
     otError error = OT_ERROR_NONE;
     uint8_t flags = 0;
@@ -2486,8 +2474,7 @@
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE || OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
-template <typename InterfaceType, typename ProcessContextType>
-uint8_t RadioSpinel<InterfaceType, ProcessContextType>::GetCslAccuracy(void)
+template <typename InterfaceType> uint8_t RadioSpinel<InterfaceType>::GetCslAccuracy(void)
 {
     uint8_t accuracy = UINT8_MAX;
     otError error    = Get(SPINEL_PROP_RCP_CSL_ACCURACY, SPINEL_DATATYPE_UINT8_S, &accuracy);
@@ -2498,8 +2485,7 @@
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
-template <typename InterfaceType, typename ProcessContextType>
-uint8_t RadioSpinel<InterfaceType, ProcessContextType>::GetCslUncertainty(void)
+template <typename InterfaceType> uint8_t RadioSpinel<InterfaceType>::GetCslUncertainty(void)
 {
     uint8_t uncertainty = UINT8_MAX;
     otError error       = Get(SPINEL_PROP_RCP_CSL_UNCERTAINTY, SPINEL_DATATYPE_UINT8_S, &uncertainty);
@@ -2509,5 +2495,682 @@
 }
 #endif
 
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::AddCalibratedPower(uint8_t        aChannel,
+                                                       int16_t        aActualPower,
+                                                       const uint8_t *aRawPowerSetting,
+                                                       uint16_t       aRawPowerSettingLength)
+{
+    otError error;
+
+    assert(aRawPowerSetting != nullptr);
+    SuccessOrExit(error = Insert(SPINEL_PROP_PHY_CALIBRATED_POWER,
+                                 SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT16_S SPINEL_DATATYPE_DATA_WLEN_S, aChannel,
+                                 aActualPower, aRawPowerSetting, aRawPowerSettingLength));
+
+exit:
+    return error;
+}
+
+template <typename InterfaceType> otError RadioSpinel<InterfaceType>::ClearCalibratedPowers(void)
+{
+    return Set(SPINEL_PROP_PHY_CALIBRATED_POWER, nullptr);
+}
+
+template <typename InterfaceType>
+otError RadioSpinel<InterfaceType>::SetChannelTargetPower(uint8_t aChannel, int16_t aTargetPower)
+{
+    otError error = OT_ERROR_NONE;
+    VerifyOrExit(aChannel >= Radio::kChannelMin && aChannel <= Radio::kChannelMax, error = OT_ERROR_INVALID_ARGS);
+    error =
+        Set(SPINEL_PROP_PHY_CHAN_TARGET_POWER, SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT16_S, aChannel, aTargetPower);
+
+exit:
+    return error;
+}
+#endif // OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+
+template <typename InterfaceType>
+uint32_t RadioSpinel<InterfaceType>::Snprintf(char *aDest, uint32_t aSize, const char *aFormat, ...)
+{
+    int     len;
+    va_list args;
+
+    va_start(args, aFormat);
+    len = vsnprintf(aDest, static_cast<size_t>(aSize), aFormat, args);
+    va_end(args);
+
+    return (len < 0) ? 0 : Min(static_cast<uint32_t>(len), aSize - 1);
+}
+
+template <typename InterfaceType>
+void RadioSpinel<InterfaceType>::LogSpinelFrame(const uint8_t *aFrame, uint16_t aLength, bool aTx)
+{
+    otError           error                               = OT_ERROR_NONE;
+    char              buf[OPENTHREAD_CONFIG_LOG_MAX_SIZE] = {0};
+    spinel_ssize_t    unpacked;
+    uint8_t           header;
+    uint32_t          cmd;
+    spinel_prop_key_t key;
+    uint8_t          *data;
+    spinel_size_t     len;
+    const char       *prefix = nullptr;
+    char             *start  = buf;
+    char             *end    = buf + sizeof(buf);
+
+    VerifyOrExit(otLoggingGetLevel() >= OT_LOG_LEVEL_DEBG);
+
+    prefix   = aTx ? "Sent spinel frame" : "Received spinel frame";
+    unpacked = spinel_datatype_unpack(aFrame, aLength, "CiiD", &header, &cmd, &key, &data, &len);
+    VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+    start += Snprintf(start, static_cast<uint32_t>(end - start), "%s, flg:0x%x, tid:%u, cmd:%s", prefix,
+                      SPINEL_HEADER_GET_FLAG(header), SPINEL_HEADER_GET_TID(header), spinel_command_to_cstr(cmd));
+    VerifyOrExit(cmd != SPINEL_CMD_RESET);
+
+    start += Snprintf(start, static_cast<uint32_t>(end - start), ", key:%s", spinel_prop_key_to_cstr(key));
+    VerifyOrExit(cmd != SPINEL_CMD_PROP_VALUE_GET);
+
+    switch (key)
+    {
+    case SPINEL_PROP_LAST_STATUS:
+    {
+        spinel_status_t status;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT_PACKED_S, &status);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", status:%s", spinel_status_to_cstr(status));
+    }
+    break;
+
+    case SPINEL_PROP_MAC_RAW_STREAM_ENABLED:
+    case SPINEL_PROP_MAC_SRC_MATCH_ENABLED:
+    case SPINEL_PROP_PHY_ENABLED:
+    case SPINEL_PROP_RADIO_COEX_ENABLE:
+    {
+        bool enabled;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_BOOL_S, &enabled);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", enabled:%u", enabled);
+    }
+    break;
+
+    case SPINEL_PROP_PHY_CCA_THRESHOLD:
+    case SPINEL_PROP_PHY_FEM_LNA_GAIN:
+    case SPINEL_PROP_PHY_RX_SENSITIVITY:
+    case SPINEL_PROP_PHY_RSSI:
+    case SPINEL_PROP_PHY_TX_POWER:
+    {
+        const char *name = nullptr;
+        int8_t      value;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_INT8_S, &value);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+        switch (key)
+        {
+        case SPINEL_PROP_PHY_TX_POWER:
+            name = "power";
+            break;
+        case SPINEL_PROP_PHY_CCA_THRESHOLD:
+            name = "threshold";
+            break;
+        case SPINEL_PROP_PHY_FEM_LNA_GAIN:
+            name = "gain";
+            break;
+        case SPINEL_PROP_PHY_RX_SENSITIVITY:
+            name = "sensitivity";
+            break;
+        case SPINEL_PROP_PHY_RSSI:
+            name = "rssi";
+            break;
+        }
+
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", %s:%d", name, value);
+    }
+    break;
+
+    case SPINEL_PROP_MAC_PROMISCUOUS_MODE:
+    case SPINEL_PROP_MAC_SCAN_STATE:
+    case SPINEL_PROP_PHY_CHAN:
+    case SPINEL_PROP_RCP_CSL_ACCURACY:
+    case SPINEL_PROP_RCP_CSL_UNCERTAINTY:
+    {
+        const char *name = nullptr;
+        uint8_t     value;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT8_S, &value);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+        switch (key)
+        {
+        case SPINEL_PROP_MAC_SCAN_STATE:
+            name = "state";
+            break;
+        case SPINEL_PROP_RCP_CSL_ACCURACY:
+            name = "accuracy";
+            break;
+        case SPINEL_PROP_RCP_CSL_UNCERTAINTY:
+            name = "uncertainty";
+            break;
+        case SPINEL_PROP_MAC_PROMISCUOUS_MODE:
+            name = "mode";
+            break;
+        case SPINEL_PROP_PHY_CHAN:
+            name = "channel";
+            break;
+        }
+
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", %s:%u", name, value);
+    }
+    break;
+
+    case SPINEL_PROP_MAC_15_4_PANID:
+    case SPINEL_PROP_MAC_15_4_SADDR:
+    case SPINEL_PROP_MAC_SCAN_PERIOD:
+    case SPINEL_PROP_PHY_REGION_CODE:
+    {
+        const char *name = nullptr;
+        uint16_t    value;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT16_S, &value);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+        switch (key)
+        {
+        case SPINEL_PROP_MAC_SCAN_PERIOD:
+            name = "period";
+            break;
+        case SPINEL_PROP_PHY_REGION_CODE:
+            name = "region";
+            break;
+        case SPINEL_PROP_MAC_15_4_SADDR:
+            name = "saddr";
+            break;
+        case SPINEL_PROP_MAC_SRC_MATCH_SHORT_ADDRESSES:
+            name = "saddr";
+            break;
+        case SPINEL_PROP_MAC_15_4_PANID:
+            name = "panid";
+            break;
+        }
+
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", %s:0x%04x", name, value);
+    }
+    break;
+
+    case SPINEL_PROP_MAC_SRC_MATCH_SHORT_ADDRESSES:
+    {
+        uint16_t saddr;
+
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", saddr:");
+
+        if (len < sizeof(saddr))
+        {
+            start += Snprintf(start, static_cast<uint32_t>(end - start), "none");
+        }
+        else
+        {
+            while (len >= sizeof(saddr))
+            {
+                unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT16_S, &saddr);
+                VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+                data += unpacked;
+                len -= static_cast<spinel_size_t>(unpacked);
+                start += Snprintf(start, static_cast<uint32_t>(end - start), "0x%04x ", saddr);
+            }
+        }
+    }
+    break;
+
+    case SPINEL_PROP_RCP_MAC_FRAME_COUNTER:
+    case SPINEL_PROP_RCP_TIMESTAMP:
+    {
+        const char *name;
+        uint32_t    value;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT32_S, &value);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+        name = (key == SPINEL_PROP_RCP_TIMESTAMP) ? "timestamp" : "counter";
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", %s:%u", name, value);
+    }
+    break;
+
+    case SPINEL_PROP_RADIO_CAPS:
+    case SPINEL_PROP_RCP_API_VERSION:
+    case SPINEL_PROP_RCP_MIN_HOST_API_VERSION:
+    {
+        const char  *name;
+        unsigned int value;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT_PACKED_S, &value);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+        switch (key)
+        {
+        case SPINEL_PROP_RADIO_CAPS:
+            name = "caps";
+            break;
+        case SPINEL_PROP_RCP_API_VERSION:
+            name = "version";
+            break;
+        case SPINEL_PROP_RCP_MIN_HOST_API_VERSION:
+            name = "min-host-version";
+            break;
+        default:
+            name = "";
+            break;
+        }
+
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", %s:%u", name, value);
+    }
+    break;
+
+    case SPINEL_PROP_MAC_ENERGY_SCAN_RESULT:
+    case SPINEL_PROP_PHY_CHAN_MAX_POWER:
+    {
+        const char *name;
+        uint8_t     channel;
+        int8_t      value;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT8_S, &channel, &value);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+        name = (key == SPINEL_PROP_MAC_ENERGY_SCAN_RESULT) ? "rssi" : "power";
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", channel:%u, %s:%d", channel, name, value);
+    }
+    break;
+
+    case SPINEL_PROP_CAPS:
+    {
+        unsigned int capability;
+
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", caps:");
+
+        while (len > 0)
+        {
+            unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT_PACKED_S, &capability);
+            VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+            data += unpacked;
+            len -= static_cast<spinel_size_t>(unpacked);
+            start += Snprintf(start, static_cast<uint32_t>(end - start), "%s ", spinel_capability_to_cstr(capability));
+        }
+    }
+    break;
+
+    case SPINEL_PROP_PROTOCOL_VERSION:
+    {
+        unsigned int major;
+        unsigned int minor;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT_PACKED_S SPINEL_DATATYPE_UINT_PACKED_S,
+                                          &major, &minor);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", major:%u, minor:%u", major, minor);
+    }
+    break;
+
+    case SPINEL_PROP_PHY_CHAN_PREFERRED:
+    case SPINEL_PROP_PHY_CHAN_SUPPORTED:
+    {
+        uint8_t        maskBuffer[kChannelMaskBufferSize];
+        uint32_t       channelMask = 0;
+        const uint8_t *maskData    = maskBuffer;
+        spinel_size_t  maskLength  = sizeof(maskBuffer);
+
+        unpacked = spinel_datatype_unpack_in_place(data, len, SPINEL_DATATYPE_DATA_S, maskBuffer, &maskLength);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+        while (maskLength > 0)
+        {
+            uint8_t channel;
+
+            unpacked = spinel_datatype_unpack(maskData, maskLength, SPINEL_DATATYPE_UINT8_S, &channel);
+            VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+            VerifyOrExit(channel < kChannelMaskBufferSize, error = OT_ERROR_PARSE);
+            channelMask |= (1UL << channel);
+
+            maskData += unpacked;
+            maskLength -= static_cast<spinel_size_t>(unpacked);
+        }
+
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", channelMask:0x%08x", channelMask);
+    }
+    break;
+
+    case SPINEL_PROP_NCP_VERSION:
+    {
+        const char *version;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UTF8_S, &version);
+        VerifyOrExit(unpacked >= 0, error = OT_ERROR_PARSE);
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", version:%s", version);
+    }
+    break;
+
+    case SPINEL_PROP_STREAM_RAW:
+    {
+        otRadioFrame frame;
+
+        if (cmd == SPINEL_CMD_PROP_VALUE_IS)
+        {
+            uint16_t     flags;
+            int8_t       noiseFloor;
+            unsigned int receiveError;
+
+            unpacked = spinel_datatype_unpack(data, len,
+                                              SPINEL_DATATYPE_DATA_WLEN_S                          // Frame
+                                                  SPINEL_DATATYPE_INT8_S                           // RSSI
+                                                      SPINEL_DATATYPE_INT8_S                       // Noise Floor
+                                                          SPINEL_DATATYPE_UINT16_S                 // Flags
+                                                              SPINEL_DATATYPE_STRUCT_S(            // PHY-data
+                                                                  SPINEL_DATATYPE_UINT8_S          // 802.15.4 channel
+                                                                      SPINEL_DATATYPE_UINT8_S      // 802.15.4 LQI
+                                                                          SPINEL_DATATYPE_UINT64_S // Timestamp (us).
+                                                                  ) SPINEL_DATATYPE_STRUCT_S(      // Vendor-data
+                                                                  SPINEL_DATATYPE_UINT_PACKED_S    // Receive error
+                                                                  ),
+                                              &frame.mPsdu, &frame.mLength, &frame.mInfo.mRxInfo.mRssi, &noiseFloor,
+                                              &flags, &frame.mChannel, &frame.mInfo.mRxInfo.mLqi,
+                                              &frame.mInfo.mRxInfo.mTimestamp, &receiveError);
+            VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+            start += Snprintf(start, static_cast<uint32_t>(end - start), ", len:%u, rssi:%d ...", frame.mLength,
+                              frame.mInfo.mRxInfo.mRssi);
+            otLogDebgPlat("%s", buf);
+
+            start = buf;
+            start += Snprintf(start, static_cast<uint32_t>(end - start),
+                              "... noise:%d, flags:0x%04x, channel:%u, lqi:%u, timestamp:%lu, rxerr:%u", noiseFloor,
+                              flags, frame.mChannel, frame.mInfo.mRxInfo.mLqi,
+                              static_cast<unsigned long>(frame.mInfo.mRxInfo.mTimestamp), receiveError);
+        }
+        else if (cmd == SPINEL_CMD_PROP_VALUE_SET)
+        {
+            bool csmaCaEnabled;
+            bool isHeaderUpdated;
+            bool isARetx;
+            bool skipAes;
+
+            unpacked = spinel_datatype_unpack(
+                data, len,
+                SPINEL_DATATYPE_DATA_WLEN_S                                   // Frame data
+                    SPINEL_DATATYPE_UINT8_S                                   // Channel
+                        SPINEL_DATATYPE_UINT8_S                               // MaxCsmaBackoffs
+                            SPINEL_DATATYPE_UINT8_S                           // MaxFrameRetries
+                                SPINEL_DATATYPE_BOOL_S                        // CsmaCaEnabled
+                                    SPINEL_DATATYPE_BOOL_S                    // IsHeaderUpdated
+                                        SPINEL_DATATYPE_BOOL_S                // IsARetx
+                                            SPINEL_DATATYPE_BOOL_S            // SkipAes
+                                                SPINEL_DATATYPE_UINT32_S      // TxDelay
+                                                    SPINEL_DATATYPE_UINT32_S, // TxDelayBaseTime
+                &frame.mPsdu, &frame.mLength, &frame.mChannel, &frame.mInfo.mTxInfo.mMaxCsmaBackoffs,
+                &frame.mInfo.mTxInfo.mMaxFrameRetries, &csmaCaEnabled, &isHeaderUpdated, &isARetx, &skipAes,
+                &frame.mInfo.mTxInfo.mTxDelay, &frame.mInfo.mTxInfo.mTxDelayBaseTime);
+
+            VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+            start += Snprintf(start, static_cast<uint32_t>(end - start),
+                              ", len:%u, channel:%u, maxbackoffs:%u, maxretries:%u ...", frame.mLength, frame.mChannel,
+                              frame.mInfo.mTxInfo.mMaxCsmaBackoffs, frame.mInfo.mTxInfo.mMaxFrameRetries);
+            otLogDebgPlat("%s", buf);
+
+            start = buf;
+            start += Snprintf(start, static_cast<uint32_t>(end - start),
+                              "... csmaCaEnabled:%u, isHeaderUpdated:%u, isARetx:%u, skipAes:%u"
+                              ", txDelay:%u, txDelayBase:%u",
+                              csmaCaEnabled, isHeaderUpdated, isARetx, skipAes, frame.mInfo.mTxInfo.mTxDelay,
+                              frame.mInfo.mTxInfo.mTxDelayBaseTime);
+        }
+    }
+    break;
+
+    case SPINEL_PROP_STREAM_DEBUG:
+    {
+        char          debugString[OPENTHREAD_CONFIG_NCP_SPINEL_LOG_MAX_SIZE + 1];
+        spinel_size_t stringLength = sizeof(debugString);
+
+        unpacked = spinel_datatype_unpack_in_place(data, len, SPINEL_DATATYPE_DATA_S, debugString, &stringLength);
+        assert(stringLength < sizeof(debugString));
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        debugString[stringLength] = '\0';
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", debug:%s", debugString);
+    }
+    break;
+
+    case SPINEL_PROP_STREAM_LOG:
+    {
+        const char *logString;
+        uint8_t     logLevel;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UTF8_S, &logString);
+        VerifyOrExit(unpacked >= 0, error = OT_ERROR_PARSE);
+        data += unpacked;
+        len -= static_cast<spinel_size_t>(unpacked);
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT8_S, &logLevel);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", level:%u, log:%s", logLevel, logString);
+    }
+    break;
+
+    case SPINEL_PROP_NEST_STREAM_MFG:
+    {
+        const char *output;
+        size_t      outputLen;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UTF8_S, &output, &outputLen);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", diag:%s", output);
+    }
+    break;
+
+    case SPINEL_PROP_RCP_MAC_KEY:
+    {
+        uint8_t      keyIdMode;
+        uint8_t      keyId;
+        otMacKey     prevKey;
+        unsigned int prevKeyLen = sizeof(otMacKey);
+        otMacKey     currKey;
+        unsigned int currKeyLen = sizeof(otMacKey);
+        otMacKey     nextKey;
+        unsigned int nextKeyLen = sizeof(otMacKey);
+
+        unpacked = spinel_datatype_unpack(data, len,
+                                          SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_DATA_WLEN_S
+                                              SPINEL_DATATYPE_DATA_WLEN_S SPINEL_DATATYPE_DATA_WLEN_S,
+                                          &keyIdMode, &keyId, prevKey.m8, &prevKeyLen, currKey.m8, &currKeyLen,
+                                          nextKey.m8, &nextKeyLen);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        start += Snprintf(start, static_cast<uint32_t>(end - start),
+                          ", keyIdMode:%u, keyId:%u, prevKey:***, currKey:***, nextKey:***", keyIdMode, keyId);
+    }
+    break;
+
+    case SPINEL_PROP_HWADDR:
+    case SPINEL_PROP_MAC_15_4_LADDR:
+    {
+        const char *name                    = nullptr;
+        uint8_t     m8[OT_EXT_ADDRESS_SIZE] = {0};
+
+        unpacked = spinel_datatype_unpack_in_place(data, len, SPINEL_DATATYPE_EUI64_S, &m8[0]);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+        name = (key == SPINEL_PROP_HWADDR) ? "eui64" : "laddr";
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", %s:%02x%02x%02x%02x%02x%02x%02x%02x", name,
+                          m8[0], m8[1], m8[2], m8[3], m8[4], m8[5], m8[6], m8[7]);
+    }
+    break;
+
+    case SPINEL_PROP_MAC_SRC_MATCH_EXTENDED_ADDRESSES:
+    {
+        uint8_t m8[OT_EXT_ADDRESS_SIZE];
+
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", extaddr:");
+
+        if (len < sizeof(m8))
+        {
+            start += Snprintf(start, static_cast<uint32_t>(end - start), "none");
+        }
+        else
+        {
+            while (len >= sizeof(m8))
+            {
+                unpacked = spinel_datatype_unpack_in_place(data, len, SPINEL_DATATYPE_EUI64_S, m8);
+                VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+                data += unpacked;
+                len -= static_cast<spinel_size_t>(unpacked);
+                start += Snprintf(start, static_cast<uint32_t>(end - start), "%02x%02x%02x%02x%02x%02x%02x%02x ", m8[0],
+                                  m8[1], m8[2], m8[3], m8[4], m8[5], m8[6], m8[7]);
+            }
+        }
+    }
+    break;
+
+    case SPINEL_PROP_RADIO_COEX_METRICS:
+    {
+        otRadioCoexMetrics metrics;
+        unpacked = spinel_datatype_unpack(
+            data, len,
+            SPINEL_DATATYPE_STRUCT_S(                                    // Tx Coex Metrics Structure
+                SPINEL_DATATYPE_UINT32_S                                 // NumTxRequest
+                    SPINEL_DATATYPE_UINT32_S                             // NumTxGrantImmediate
+                        SPINEL_DATATYPE_UINT32_S                         // NumTxGrantWait
+                            SPINEL_DATATYPE_UINT32_S                     // NumTxGrantWaitActivated
+                                SPINEL_DATATYPE_UINT32_S                 // NumTxGrantWaitTimeout
+                                    SPINEL_DATATYPE_UINT32_S             // NumTxGrantDeactivatedDuringRequest
+                                        SPINEL_DATATYPE_UINT32_S         // NumTxDelayedGrant
+                                            SPINEL_DATATYPE_UINT32_S     // AvgTxRequestToGrantTime
+                ) SPINEL_DATATYPE_STRUCT_S(                              // Rx Coex Metrics Structure
+                SPINEL_DATATYPE_UINT32_S                                 // NumRxRequest
+                    SPINEL_DATATYPE_UINT32_S                             // NumRxGrantImmediate
+                        SPINEL_DATATYPE_UINT32_S                         // NumRxGrantWait
+                            SPINEL_DATATYPE_UINT32_S                     // NumRxGrantWaitActivated
+                                SPINEL_DATATYPE_UINT32_S                 // NumRxGrantWaitTimeout
+                                    SPINEL_DATATYPE_UINT32_S             // NumRxGrantDeactivatedDuringRequest
+                                        SPINEL_DATATYPE_UINT32_S         // NumRxDelayedGrant
+                                            SPINEL_DATATYPE_UINT32_S     // AvgRxRequestToGrantTime
+                                                SPINEL_DATATYPE_UINT32_S // NumRxGrantNone
+                ) SPINEL_DATATYPE_BOOL_S                                 // Stopped
+                SPINEL_DATATYPE_UINT32_S,                                // NumGrantGlitch
+            &metrics.mNumTxRequest, &metrics.mNumTxGrantImmediate, &metrics.mNumTxGrantWait,
+            &metrics.mNumTxGrantWaitActivated, &metrics.mNumTxGrantWaitTimeout,
+            &metrics.mNumTxGrantDeactivatedDuringRequest, &metrics.mNumTxDelayedGrant,
+            &metrics.mAvgTxRequestToGrantTime, &metrics.mNumRxRequest, &metrics.mNumRxGrantImmediate,
+            &metrics.mNumRxGrantWait, &metrics.mNumRxGrantWaitActivated, &metrics.mNumRxGrantWaitTimeout,
+            &metrics.mNumRxGrantDeactivatedDuringRequest, &metrics.mNumRxDelayedGrant,
+            &metrics.mAvgRxRequestToGrantTime, &metrics.mNumRxGrantNone, &metrics.mStopped, &metrics.mNumGrantGlitch);
+
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+        otLogDebgPlat("%s ...", buf);
+        otLogDebgPlat(" txRequest:%lu", ToUlong(metrics.mNumTxRequest));
+        otLogDebgPlat(" txGrantImmediate:%lu", ToUlong(metrics.mNumTxGrantImmediate));
+        otLogDebgPlat(" txGrantWait:%lu", ToUlong(metrics.mNumTxGrantWait));
+        otLogDebgPlat(" txGrantWaitActivated:%lu", ToUlong(metrics.mNumTxGrantWaitActivated));
+        otLogDebgPlat(" txGrantWaitTimeout:%lu", ToUlong(metrics.mNumTxGrantWaitTimeout));
+        otLogDebgPlat(" txGrantDeactivatedDuringRequest:%lu", ToUlong(metrics.mNumTxGrantDeactivatedDuringRequest));
+        otLogDebgPlat(" txDelayedGrant:%lu", ToUlong(metrics.mNumTxDelayedGrant));
+        otLogDebgPlat(" avgTxRequestToGrantTime:%lu", ToUlong(metrics.mAvgTxRequestToGrantTime));
+        otLogDebgPlat(" rxRequest:%lu", ToUlong(metrics.mNumRxRequest));
+        otLogDebgPlat(" rxGrantImmediate:%lu", ToUlong(metrics.mNumRxGrantImmediate));
+        otLogDebgPlat(" rxGrantWait:%lu", ToUlong(metrics.mNumRxGrantWait));
+        otLogDebgPlat(" rxGrantWaitActivated:%lu", ToUlong(metrics.mNumRxGrantWaitActivated));
+        otLogDebgPlat(" rxGrantWaitTimeout:%lu", ToUlong(metrics.mNumRxGrantWaitTimeout));
+        otLogDebgPlat(" rxGrantDeactivatedDuringRequest:%lu", ToUlong(metrics.mNumRxGrantDeactivatedDuringRequest));
+        otLogDebgPlat(" rxDelayedGrant:%lu", ToUlong(metrics.mNumRxDelayedGrant));
+        otLogDebgPlat(" avgRxRequestToGrantTime:%lu", ToUlong(metrics.mAvgRxRequestToGrantTime));
+        otLogDebgPlat(" rxGrantNone:%lu", ToUlong(metrics.mNumRxGrantNone));
+        otLogDebgPlat(" stopped:%u", metrics.mStopped);
+
+        start = buf;
+        start += Snprintf(start, static_cast<uint32_t>(end - start), " grantGlitch:%u", metrics.mNumGrantGlitch);
+    }
+    break;
+
+    case SPINEL_PROP_MAC_SCAN_MASK:
+    {
+        constexpr uint8_t kNumChannels = 16;
+        uint8_t           channels[kNumChannels];
+        spinel_size_t     size;
+
+        unpacked = spinel_datatype_unpack(data, len, SPINEL_DATATYPE_DATA_S, channels, &size);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", channels:");
+
+        for (uint8_t i = 0; i < size; i++)
+        {
+            start += Snprintf(start, static_cast<uint32_t>(end - start), "%u ", channels[i]);
+        }
+    }
+    break;
+
+    case SPINEL_PROP_RCP_ENH_ACK_PROBING:
+    {
+        uint16_t saddr;
+        uint8_t  m8[OT_EXT_ADDRESS_SIZE];
+        uint8_t  flags;
+
+        unpacked = spinel_datatype_unpack(
+            data, len, SPINEL_DATATYPE_UINT16_S SPINEL_DATATYPE_EUI64_S SPINEL_DATATYPE_UINT8_S, &saddr, m8, &flags);
+
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        start += Snprintf(start, static_cast<uint32_t>(end - start),
+                          ", saddr:%04x, extaddr:%02x%02x%02x%02x%02x%02x%02x%02x, flags:0x%02x", saddr, m8[0], m8[1],
+                          m8[2], m8[3], m8[4], m8[5], m8[6], m8[7], flags);
+    }
+    break;
+
+    case SPINEL_PROP_PHY_CALIBRATED_POWER:
+    {
+        if (cmd == SPINEL_CMD_PROP_VALUE_INSERT)
+        {
+            uint8_t      channel;
+            int16_t      actualPower;
+            uint8_t     *rawPowerSetting;
+            unsigned int rawPowerSettingLength;
+
+            unpacked = spinel_datatype_unpack(
+                data, len, SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT16_S SPINEL_DATATYPE_DATA_WLEN_S, &channel,
+                &actualPower, &rawPowerSetting, &rawPowerSettingLength);
+            VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+
+            start += Snprintf(start, static_cast<uint32_t>(end - start),
+                              ", ch:%u, actualPower:%d, rawPowerSetting:", channel, actualPower);
+            for (uint16_t i = 0; i < rawPowerSettingLength; i++)
+            {
+                start += Snprintf(start, static_cast<uint32_t>(end - start), "%02x", rawPowerSetting[i]);
+            }
+        }
+    }
+    break;
+
+    case SPINEL_PROP_PHY_CHAN_TARGET_POWER:
+    {
+        uint8_t channel;
+        int16_t targetPower;
+
+        unpacked =
+            spinel_datatype_unpack(data, len, SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT16_S, &channel, &targetPower);
+        VerifyOrExit(unpacked > 0, error = OT_ERROR_PARSE);
+        start += Snprintf(start, static_cast<uint32_t>(end - start), ", ch:%u, targetPower:%d", channel, targetPower);
+    }
+    break;
+    }
+
+exit:
+    if (error == OT_ERROR_NONE)
+    {
+        otLogDebgPlat("%s", buf);
+    }
+    else
+    {
+        otLogDebgPlat("%s, failed to parse spinel frame !", prefix);
+    }
+
+    return;
+}
+
 } // namespace Spinel
 } // namespace ot
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.c b/src/lib/spinel/spinel.c
index 190381d..5cf3836 100644
--- a/src/lib/spinel/spinel.c
+++ b/src/lib/spinel/spinel.c
@@ -340,8 +340,8 @@
 static spinel_ssize_t spinel_datatype_vunpack_(bool           in_place,
                                                const uint8_t *data_in,
                                                spinel_size_t  data_len,
-                                               const char *   pack_format,
-                                               va_list_obj *  args)
+                                               const char    *pack_format,
+                                               va_list_obj   *args)
 {
     spinel_ssize_t ret = 0;
 
@@ -528,7 +528,7 @@
 
         case SPINEL_DATATYPE_UINT_PACKED_C:
         {
-            unsigned int * arg_ptr = va_arg(args->obj, unsigned int *);
+            unsigned int  *arg_ptr = va_arg(args->obj, unsigned int *);
             spinel_ssize_t pui_len = spinel_packed_uint_decode(data_in, data_len, arg_ptr);
 
             // Range check
@@ -564,7 +564,7 @@
 
             if (in_place)
             {
-                char * arg     = va_arg(args->obj, char *);
+                char  *arg     = va_arg(args->obj, char *);
                 size_t len_arg = va_arg(args->obj, size_t);
                 if (arg)
                 {
@@ -593,8 +593,8 @@
             spinel_ssize_t pui_len       = 0;
             uint16_t       block_len     = 0;
             const uint8_t *block_ptr     = data_in;
-            void *         arg_ptr       = va_arg(args->obj, void *);
-            unsigned int * block_len_ptr = va_arg(args->obj, unsigned int *);
+            void          *arg_ptr       = va_arg(args->obj, void *);
+            unsigned int  *block_len_ptr = va_arg(args->obj, unsigned int *);
             char           nextformat    = *spinel_next_packed_datatype(pack_format);
 
             if ((pack_format[0] == SPINEL_DATATYPE_DATA_WLEN_C) || ((nextformat != 0) && (nextformat != ')')))
@@ -704,7 +704,7 @@
 
 spinel_ssize_t spinel_datatype_unpack_in_place(const uint8_t *data_in,
                                                spinel_size_t  data_len,
-                                               const char *   pack_format,
+                                               const char    *pack_format,
                                                ...)
 {
     spinel_ssize_t ret;
@@ -731,7 +731,7 @@
 
 spinel_ssize_t spinel_datatype_vunpack_in_place(const uint8_t *data_in,
                                                 spinel_size_t  data_len,
-                                                const char *   pack_format,
+                                                const char    *pack_format,
                                                 va_list        args)
 {
     spinel_ssize_t ret;
@@ -746,7 +746,7 @@
 
 spinel_ssize_t spinel_datatype_vunpack(const uint8_t *data_in,
                                        spinel_size_t  data_len,
-                                       const char *   pack_format,
+                                       const char    *pack_format,
                                        va_list        args)
 {
     spinel_ssize_t ret;
@@ -759,10 +759,10 @@
     return ret;
 }
 
-static spinel_ssize_t spinel_datatype_vpack_(uint8_t *     data_out,
+static spinel_ssize_t spinel_datatype_vpack_(uint8_t      *data_out,
                                              spinel_size_t data_len_max,
-                                             const char *  pack_format,
-                                             va_list_obj * args)
+                                             const char   *pack_format,
+                                             va_list_obj  *args)
 {
     spinel_ssize_t ret = 0;
 
@@ -1128,9 +1128,9 @@
     return ret;
 }
 
-spinel_ssize_t spinel_datatype_vpack(uint8_t *     data_out,
+spinel_ssize_t spinel_datatype_vpack(uint8_t      *data_out,
                                      spinel_size_t data_len_max,
-                                     const char *  pack_format,
+                                     const char   *pack_format,
                                      va_list       args)
 {
     int         ret;
@@ -1235,6 +1235,9 @@
         {SPINEL_PROP_PHY_PCAP_ENABLED, "PHY_PCAP_ENABLED"},
         {SPINEL_PROP_PHY_CHAN_PREFERRED, "PHY_CHAN_PREFERRED"},
         {SPINEL_PROP_PHY_CHAN_MAX_POWER, "PHY_CHAN_MAX_POWER"},
+        {SPINEL_PROP_PHY_REGION_CODE, "PHY_REGION_CODE"},
+        {SPINEL_PROP_PHY_CALIBRATED_POWER, "PHY_CALIBRATED_POWER"},
+        {SPINEL_PROP_PHY_CHAN_TARGET_POWER, "PHY_CHAN_TARGET_POWER"},
         {SPINEL_PROP_JAM_DETECT_ENABLE, "JAM_DETECT_ENABLE"},
         {SPINEL_PROP_JAM_DETECTED, "JAM_DETECTED"},
         {SPINEL_PROP_JAM_DETECT_RSSI_THRESHOLD, "JAM_DETECT_RSSI_THRESHOLD"},
@@ -1400,6 +1403,7 @@
         {SPINEL_PROP_CHILD_SUPERVISION_INTERVAL, "CHILD_SUPERVISION_INTERVAL"},
         {SPINEL_PROP_CHILD_SUPERVISION_CHECK_TIMEOUT, "CHILD_SUPERVISION_CHECK_TIMEOUT"},
         {SPINEL_PROP_RCP_VERSION, "RCP_VERSION"},
+        {SPINEL_PROP_RCP_TIMESTAMP, "TIMESTAMP"},
         {SPINEL_PROP_RCP_ENH_ACK_PROBING, "ENH_ACK_PROBING"},
         {SPINEL_PROP_RCP_CSL_ACCURACY, "CSL_ACCURACY"},
         {SPINEL_PROP_RCP_CSL_UNCERTAINTY, "CSL_UNCERTAINTY"},
@@ -1422,6 +1426,7 @@
         {SPINEL_PROP_SERVER_SERVICES, "SERVER_SERVICES"},
         {SPINEL_PROP_SERVER_LEADER_SERVICES, "SERVER_LEADER_SERVICES"},
         {SPINEL_PROP_RCP_API_VERSION, "RCP_API_VERSION"},
+        {SPINEL_PROP_RCP_MIN_HOST_API_VERSION, "RCP_MIN_HOST_API_VERSION"},
         {SPINEL_PROP_UART_BITRATE, "UART_BITRATE"},
         {SPINEL_PROP_UART_XON_XOFF, "UART_XON_XOFF"},
         {SPINEL_PROP_15_4_PIB_PHY_CHANNELS_SUPPORTED, "15_4_PIB_PHY_CHANNELS_SUPPORTED"},
@@ -1479,8 +1484,6 @@
         {SPINEL_PROP_CNTR_ALL_IP_COUNTERS, "CNTR_ALL_IP_COUNTERS"},
         {SPINEL_PROP_CNTR_MAC_RETRY_HISTOGRAM, "CNTR_MAC_RETRY_HISTOGRAM"},
         {SPINEL_PROP_NEST_STREAM_MFG, "NEST_STREAM_MFG"},
-        {SPINEL_PROP_NEST_LEGACY_ULA_PREFIX, "NEST_LEGACY_ULA_PREFIX"},
-        {SPINEL_PROP_NEST_LEGACY_LAST_NODE_JOINED, "NEST_LEGACY_LAST_NODE_JOINED"},
         {SPINEL_PROP_DEBUG_TEST_ASSERT, "DEBUG_TEST_ASSERT"},
         {SPINEL_PROP_DEBUG_NCP_LOG_LEVEL, "DEBUG_NCP_LOG_LEVEL"},
         {SPINEL_PROP_DEBUG_TEST_WATCHDOG, "DEBUG_TEST_WATCHDOG"},
@@ -1628,8 +1631,6 @@
         {SPINEL_CAP_THREAD_SERVICE, "THREAD_SERVICE"},
         {SPINEL_CAP_THREAD_CSL_RECEIVER, "THREAD_CSL_RECEIVER"},
         {SPINEL_CAP_THREAD_BACKBONE_ROUTER, "THREAD_BACKBONE_ROUTER"},
-        {SPINEL_CAP_NEST_LEGACY_INTERFACE, "NEST_LEGACY_INTERFACE"},
-        {SPINEL_CAP_NEST_LEGACY_NET_WAKE, "NEST_LEGACY_NET_WAKE"},
         {SPINEL_CAP_NEST_TRANSMIT_HOOK, "NEST_TRANSMIT_HOOK"},
         {0, NULL},
     };
@@ -1686,7 +1687,7 @@
         unsigned int          i1    = 0;
         unsigned int          i2    = 0;
         uint32_t              l     = 0;
-        const char *          str   = NULL;
+        const char           *str   = NULL;
         const spinel_eui64_t *eui64 = NULL;
 
         len = spinel_datatype_unpack(buffer, (spinel_size_t)len, "CiiLUE", &c, &i1, &i2, &l, &str, &eui64);
@@ -1806,7 +1807,7 @@
         unsigned int    i1    = 0;
         unsigned int    i2    = 0;
         uint32_t        l     = 0;
-        const char *    str   = NULL;
+        const char     *str   = NULL;
         spinel_eui64_t *eui64 = NULL;
 
         len = spinel_datatype_unpack(buffer, (spinel_size_t)len, "Cit(iL)UE", &c, &i1, &i2, &l, &str, &eui64);
@@ -1930,8 +1931,8 @@
         const uint8_t str6[] = {0xE5, 0xA2, 0x82, 0xE0, 0xA0, 0x80, 0xC2, 0x83, 0xC2, 0x80, 0xF4,
                                 0x8F, 0xBF, 0xBF, 0xF4, 0x8F, 0xBF, 0xBF, 0xDF, 0xBF, 0x21, 0x00};
 
-        const uint8_t * good_strings[] = {single1, single2, single3, single4, single5, single6, single7, single8,
-                                         str1,    str2,    str3,    str4,    str5,    str6,    NULL};
+        const uint8_t  *good_strings[] = {single1, single2, single3, single4, single5, single6, single7, single8,
+                                          str1,    str2,    str3,    str4,    str5,    str6,    NULL};
         const uint8_t **str_ptr;
 
         for (str_ptr = &good_strings[0]; *str_ptr != NULL; str_ptr++)
@@ -1961,7 +1962,7 @@
         const uint8_t bad5[] = {0x21, 0xA0, 0x00};
         const uint8_t bad6[] = {0xCE, 0xBA, 0xE1, 0xBD, 0xB9, 0xCF, 0x83, 0xCE, 0xBC, 0xCE, 0x00};
 
-        const uint8_t * bad_strings[] = {single1, single2, single3, single4, bad1, bad2, bad3, bad4, bad5, bad6, NULL};
+        const uint8_t  *bad_strings[] = {single1, single2, single3, single4, bad1, bad2, bad3, bad4, bad5, bad6, NULL};
         const uint8_t **str_ptr;
 
         for (str_ptr = &bad_strings[0]; *str_ptr != NULL; str_ptr++)
diff --git a/src/lib/spinel/spinel.h b/src/lib/spinel/spinel.h
index bc1b8d5..1f4476a 100644
--- a/src/lib/spinel/spinel.h
+++ b/src/lib/spinel/spinel.h
@@ -321,11 +321,54 @@
  *
  *   - SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION specifies the minimum spinel
  *     RCP API Version which is supported by the host-side implementation.
+ *     To reduce the backward compatibility issues, this number should be kept
+ *     as constant as possible.
  *
  *   - On start, host implementation queries the RCP API version and accepts
- *     any version number from SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION up to
- *     and including SPINEL_RCP_API_VERSION.
+ *     any version starting from SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION.
  *
+ *   - Host implementation also queries the RCP about the minimum host RCP
+ *     API version it can work with, and then checks that its own version is
+ *     within the range.
+ *
+ *   Host and RCP compatibility guideline:
+ *
+ *   - New host spinel layer should work with an older RCP firmware, i.e., host
+ *     implementation should remain backward compatible.
+ *
+ *   - Existing fields in the format of an already implemented spinel
+ *     property or command must not change.
+ *
+ *   - New fields must be appended to the end of the existing spinel format.
+ *     *  New fields for new features:
+ *          Adding a new capability flag to the otRadioCaps to indicate the new
+ *          fields. The host parses the spinel format based on the pre-fetched
+ *          otRadioCaps. The host should be able to enable/disable the feature
+ *          in runtime based on the otRadioCaps. Refer to PR4919 and PR5139.
+ *     *  New fields for changing existing implementations:
+ *          This case should be avoided as much as possible. It will cause the
+ *          compatibility issue.
+ *
+ *   - Deprecated fields must not be removed from the spinel format and they
+ *     must be set to a suitable default value.
+ *
+ *   - Adding new spinel properties.
+ *     * If the old version RCP doesn't support the new spinel property, it
+ *       must return the spinel error SPINEL_STATUS_PROP_NOT_FOUND.
+ *
+ *     * If the host can handle the new spinel property by processing the error
+ *       SPINEL_STATUS_PROP_NOT_FOUND, the API of the new spinel property must
+ *       return OT_ERROR_NOT_IMPLEMENTED or default value.
+ *
+ *     * If the host can't handle the new spinel property by processing the
+ *       error SPINEL_STATUS_PROP_NOT_FOUND, a new capability flag must be
+ *       added to the otRadioCaps to indicate whether RCP supports the new
+ *       spinel property. The host must handle the new spinel property by
+ *       processing the new capability flag.
+ *
+ *   - If none of the above methods make the new functions work, increasing the
+ *     SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION. This case should be avoided
+ *     as much as possible.
  * ---------------------------------------------------------------------------
  */
 
@@ -377,7 +420,7 @@
  * Please see section "Spinel definition compatibility guideline" for more details.
  *
  */
-#define SPINEL_RCP_API_VERSION 6
+#define SPINEL_RCP_API_VERSION 9
 
 /**
  * @def SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION
@@ -1238,9 +1281,10 @@
     SPINEL_CAP_NET_THREAD_1_2 = (SPINEL_CAP_NET__BEGIN + 2),
     SPINEL_CAP_NET__END       = 64,
 
-    SPINEL_CAP_RCP__BEGIN      = 64,
-    SPINEL_CAP_RCP_API_VERSION = (SPINEL_CAP_RCP__BEGIN + 0),
-    SPINEL_CAP_RCP__END        = 80,
+    SPINEL_CAP_RCP__BEGIN               = 64,
+    SPINEL_CAP_RCP_API_VERSION          = (SPINEL_CAP_RCP__BEGIN + 0),
+    SPINEL_CAP_RCP_MIN_HOST_API_VERSION = (SPINEL_CAP_RCP__BEGIN + 1),
+    SPINEL_CAP_RCP__END                 = 80,
 
     SPINEL_CAP_OPENTHREAD__BEGIN       = 512,
     SPINEL_CAP_MAC_ALLOWLIST           = (SPINEL_CAP_OPENTHREAD__BEGIN + 0),
@@ -1275,8 +1319,8 @@
     SPINEL_CAP_THREAD__END            = 1152,
 
     SPINEL_CAP_NEST__BEGIN           = 15296,
-    SPINEL_CAP_NEST_LEGACY_INTERFACE = (SPINEL_CAP_NEST__BEGIN + 0),
-    SPINEL_CAP_NEST_LEGACY_NET_WAKE  = (SPINEL_CAP_NEST__BEGIN + 1),
+    SPINEL_CAP_NEST_LEGACY_INTERFACE = (SPINEL_CAP_NEST__BEGIN + 0), ///< deprecated
+    SPINEL_CAP_NEST_LEGACY_NET_WAKE  = (SPINEL_CAP_NEST__BEGIN + 1), ///< deprecated
     SPINEL_CAP_NEST_TRANSMIT_HOOK    = (SPINEL_CAP_NEST__BEGIN + 2),
     SPINEL_CAP_NEST__END             = 15360,
 
@@ -1581,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
@@ -1597,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.
@@ -1703,6 +1747,28 @@
      */
     SPINEL_PROP_PHY_REGION_CODE = SPINEL_PROP_PHY__BEGIN + 12,
 
+    /// Calibrated Power Table
+    /** Format: `A(Csd)` - Insert/Set
+     *
+     *  The `Insert` command on the property inserts a calibration power entry to the calibrated power table.
+     *  The `Set` command on the property with empty payload clears the calibrated power table.
+     *
+     * Structure Parameters:
+     *  `C`: Channel.
+     *  `s`: Actual power in 0.01 dBm.
+     *  `d`: Raw power setting.
+     */
+    SPINEL_PROP_PHY_CALIBRATED_POWER = SPINEL_PROP_PHY__BEGIN + 13,
+
+    /// Target power for a channel
+    /** Format: `t(Cs)` - Write only
+     *
+     * Structure Parameters:
+     *  `C`: Channel.
+     *  `s`: Target power in 0.01 dBm.
+     */
+    SPINEL_PROP_PHY_CHAN_TARGET_POWER = SPINEL_PROP_PHY__BEGIN + 14,
+
     SPINEL_PROP_PHY__END = 0x30,
 
     SPINEL_PROP_PHY_EXT__BEGIN = 0x1200,
@@ -1925,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`.
      *
@@ -2295,7 +2361,7 @@
     SPINEL_PROP_THREAD_LEADER_ADDR = SPINEL_PROP_THREAD__BEGIN + 0,
 
     /// Thread Parent Info
-    /** Format: `ESLccCC` - Read only
+    /** Format: `ESLccCCCCC` - Read only
      *
      *  `E`: Extended address
      *  `S`: RLOC16
@@ -2304,6 +2370,9 @@
      *  `c`: Last RSSI (in dBm)
      *  `C`: Link Quality In
      *  `C`: Link Quality Out
+     *  `C`: Version
+     *  `C`: CSL clock accuracy
+     *  `C`: CSL uncertainty
      *
      */
     SPINEL_PROP_THREAD_PARENT = SPINEL_PROP_THREAD__BEGIN + 1,
@@ -2403,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
@@ -2915,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).
      *
@@ -3197,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,
@@ -3250,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,
@@ -3442,19 +3511,27 @@
      * over the radio. This allows the caller to use the radio directly.
      *
      * The frame meta data for the `CMD_PROP_VALUE_SET` contains the following
-     * optional fields.  Default values are used for all unspecified fields.
+     * fields.  Default values are used for all unspecified fields.
      *
-     *  `C` : Channel (for frame tx)
+     *  `C` : Channel (for frame tx) - MUST be included.
      *  `C` : Maximum number of backoffs attempts before declaring CCA failure
      *        (use Thread stack default if not specified)
      *  `C` : Maximum number of retries allowed after a transmission failure
      *        (use Thread stack default if not specified)
      *  `b` : Set to true to enable CSMA-CA for this packet, false otherwise.
      *        (default true).
-     *  `b` : Set to true to indicate it is a retransmission packet, false otherwise.
-     *        (default false).
-     *  `b` : Set to true to indicate that SubMac should skip AES processing, false otherwise.
-     *        (default false).
+     *  `b` : Set to true to indicate if header is updated - related to
+     *        `mIsHeaderUpdated` in `otRadioFrame` (default false).
+     *  `b` : Set to true to indicate it is a retransmission - related to
+     *        `mIsARetx` in `otRadioFrame` (default false).
+     *  `b` : Set to true to indicate security was processed on tx frame
+     *        `mIsSecurityProcessed` in `otRadioFrame` (default false).
+     *  `L` : TX delay interval used for CSL - related to `mTxDelay` in
+     *        `otRadioFrame` (default zero).
+     *  `L` : TX delay based time used for CSL - related to `mTxDelayBaseTime`
+     *        in `otRadioFrame` (default zero).
+     *  `C` : RX channel after TX done (default assumed to be same as
+     *        channel in metadata)
      *
      */
     SPINEL_PROP_STREAM_RAW = SPINEL_PROP_STREAM__BEGIN + 1,
@@ -4284,6 +4361,18 @@
      */
     SPINEL_PROP_RCP_API_VERSION = SPINEL_PROP_RCP__BEGIN + 0,
 
+    /// Min host RCP API Version number
+    /** Format: `i` (read-only)
+     *
+     * Required capability: SPINEL_CAP_RADIO and SPINEL_CAP_RCP_MIN_HOST_API_VERSION.
+     *
+     * This property gives the minimum host RCP API Version number.
+     *
+     * Please see "Spinel definition compatibility guideline" section.
+     *
+     */
+    SPINEL_PROP_RCP_MIN_HOST_API_VERSION = SPINEL_PROP_RCP__BEGIN + 1,
+
     SPINEL_PROP_RCP__END = 0xFF,
 
     SPINEL_PROP_INTERFACE__BEGIN = 0x100,
@@ -4691,9 +4780,12 @@
     SPINEL_PROP_RCP_MAC_KEY = SPINEL_PROP_RCP_EXT__BEGIN + 0,
 
     /// MAC Frame Counter
-    /** Format: `L`.
+    /** Format: `L` for read and `Lb` or `L` for write
      *
      *  `L`: MAC frame counter
+     *  'b': Optional boolean used only during write. If not provided, `false` is assumed.
+     *       If `true` counter is set only if the new value is larger than current value.
+     *       If `false` the new value is set as frame counter independent of the current value.
      *
      * The Spinel property is used to set MAC frame counter to RCP.
      *
@@ -4755,12 +4847,20 @@
 
     SPINEL_PROP_NEST_STREAM_MFG = SPINEL_PROP_NEST__BEGIN + 0,
 
-    /// The legacy network ULA prefix (8 bytes)
-    /** Format: 'D' */
+    /// The legacy network ULA prefix (8 bytes).
+    /** Format: 'D'
+     *
+     * This property is deprecated.
+     *
+     */
     SPINEL_PROP_NEST_LEGACY_ULA_PREFIX = SPINEL_PROP_NEST__BEGIN + 1,
 
     /// The EUI64 of last node joined using legacy protocol (if none, all zero EUI64 is returned).
-    /** Format: 'E' */
+    /** Format: 'E'
+     *
+     * This property is deprecated.
+     *
+     */
     SPINEL_PROP_NEST_LEGACY_LAST_NODE_JOINED = SPINEL_PROP_NEST__BEGIN + 2,
 
     SPINEL_PROP_NEST__END = 0x3C00,
@@ -4828,6 +4928,9 @@
 // ----------------------------------------------------------------------------
 
 #define SPINEL_HEADER_FLAG 0x80
+#define SPINEL_HEADER_FLAGS_SHIFT 6
+#define SPINEL_HEADER_FLAGS_MASK (3 << SPINEL_HEADER_FLAGS_SHIFT)
+#define SPINEL_HEADER_GET_FLAG(x) (((x)&SPINEL_HEADER_FLAGS_MASK) >> SPINEL_HEADER_FLAGS_SHIFT)
 
 #define SPINEL_HEADER_TID_SHIFT 0
 #define SPINEL_HEADER_TID_MASK (15 << SPINEL_HEADER_TID_SHIFT)
@@ -4915,17 +5018,17 @@
 
 #define SPINEL_MAX_UINT_PACKED 2097151
 
-SPINEL_API_EXTERN spinel_ssize_t spinel_datatype_pack(uint8_t *     data_out,
+SPINEL_API_EXTERN spinel_ssize_t spinel_datatype_pack(uint8_t      *data_out,
                                                       spinel_size_t data_len_max,
-                                                      const char *  pack_format,
+                                                      const char   *pack_format,
                                                       ...);
-SPINEL_API_EXTERN spinel_ssize_t spinel_datatype_vpack(uint8_t *     data_out,
+SPINEL_API_EXTERN spinel_ssize_t spinel_datatype_vpack(uint8_t      *data_out,
                                                        spinel_size_t data_len_max,
-                                                       const char *  pack_format,
+                                                       const char   *pack_format,
                                                        va_list       args);
 SPINEL_API_EXTERN spinel_ssize_t spinel_datatype_unpack(const uint8_t *data_in,
                                                         spinel_size_t  data_len,
-                                                        const char *   pack_format,
+                                                        const char    *pack_format,
                                                         ...);
 /**
  * This function parses spinel data similar to sscanf().
@@ -4953,11 +5056,11 @@
  */
 SPINEL_API_EXTERN spinel_ssize_t spinel_datatype_unpack_in_place(const uint8_t *data_in,
                                                                  spinel_size_t  data_len,
-                                                                 const char *   pack_format,
+                                                                 const char    *pack_format,
                                                                  ...);
 SPINEL_API_EXTERN spinel_ssize_t spinel_datatype_vunpack(const uint8_t *data_in,
                                                          spinel_size_t  data_len,
-                                                         const char *   pack_format,
+                                                         const char    *pack_format,
                                                          va_list        args);
 /**
  * This function parses spinel data similar to vsscanf().
@@ -4983,12 +5086,12 @@
  */
 SPINEL_API_EXTERN spinel_ssize_t spinel_datatype_vunpack_in_place(const uint8_t *data_in,
                                                                   spinel_size_t  data_len,
-                                                                  const char *   pack_format,
+                                                                  const char    *pack_format,
                                                                   va_list        args);
 
 SPINEL_API_EXTERN spinel_ssize_t spinel_packed_uint_decode(const uint8_t *bytes,
                                                            spinel_size_t  len,
-                                                           unsigned int * value_ptr);
+                                                           unsigned int  *value_ptr);
 SPINEL_API_EXTERN spinel_ssize_t spinel_packed_uint_encode(uint8_t *bytes, spinel_size_t len, unsigned int value);
 SPINEL_API_EXTERN spinel_ssize_t spinel_packed_uint_size(unsigned int value);
 
diff --git a/src/lib/spinel/spinel_buffer.cpp b/src/lib/spinel/spinel_buffer.cpp
index abaa2af..d4c9275 100644
--- a/src/lib/spinel/spinel_buffer.cpp
+++ b/src/lib/spinel/spinel_buffer.cpp
@@ -536,20 +536,11 @@
     return error;
 }
 
-Buffer::FrameTag Buffer::InFrameGetLastTag(void) const
-{
-    return mWriteFrameTag;
-}
+Buffer::FrameTag Buffer::InFrameGetLastTag(void) const { return mWriteFrameTag; }
 
-bool Buffer::HasFrame(Priority aPriority) const
-{
-    return mReadFrameStart[aPriority] != mWriteFrameStart[aPriority];
-}
+bool Buffer::HasFrame(Priority aPriority) const { return mReadFrameStart[aPriority] != mWriteFrameStart[aPriority]; }
 
-bool Buffer::IsEmpty(void) const
-{
-    return !HasFrame(kPriorityHigh) && !HasFrame(kPriorityLow);
-}
+bool Buffer::IsEmpty(void) const { return !HasFrame(kPriorityHigh) && !HasFrame(kPriorityLow); }
 
 void Buffer::OutFrameSelectReadDirection(void)
 {
@@ -704,10 +695,7 @@
     return error;
 }
 
-bool Buffer::OutFrameHasEnded(void)
-{
-    return (mReadState == kReadStateDone) || (mReadState == kReadStateNotActive);
-}
+bool Buffer::OutFrameHasEnded(void) { return (mReadState == kReadStateDone) || (mReadState == kReadStateNotActive); }
 
 uint8_t Buffer::OutFrameReadByte(void)
 {
diff --git a/src/lib/spinel/spinel_buffer.hpp b/src/lib/spinel/spinel_buffer.hpp
index 6377a1f..fecdb64 100644
--- a/src/lib/spinel/spinel_buffer.hpp
+++ b/src/lib/spinel/spinel_buffer.hpp
@@ -625,14 +625,14 @@
     const uint16_t mBufferLength; // Length of the buffer.
 
     BufferCallback mFrameAddedCallback;   // Callback to signal when a new frame is added
-    void *         mFrameAddedContext;    // Context passed to `mFrameAddedCallback`.
+    void          *mFrameAddedContext;    // Context passed to `mFrameAddedCallback`.
     BufferCallback mFrameRemovedCallback; // Callback to signal when a frame is removed.
-    void *         mFrameRemovedContext;  // Context passed to `mFrameRemovedCallback`.
+    void          *mFrameRemovedContext;  // Context passed to `mFrameRemovedCallback`.
 
     Direction mWriteDirection;             // Direction (priority) for current frame being read.
-    uint8_t * mWriteFrameStart[kNumPrios]; // Pointer to start of current frame being written.
-    uint8_t * mWriteSegmentHead;           // Pointer to start of current segment in the frame being written.
-    uint8_t * mWriteSegmentTail;           // Pointer to end of current segment in the frame being written.
+    uint8_t  *mWriteFrameStart[kNumPrios]; // Pointer to start of current frame being written.
+    uint8_t  *mWriteSegmentHead;           // Pointer to start of current segment in the frame being written.
+    uint8_t  *mWriteSegmentTail;           // Pointer to end of current segment in the frame being written.
     FrameTag  mWriteFrameTag;              // Tag associated with last successfully written frame.
 
     Direction mReadDirection;   // Direction (priority) for current frame being read.
@@ -647,10 +647,10 @@
 #if OPENTHREAD_SPINEL_CONFIG_OPENTHREAD_MESSAGE_ENABLE
     otMessageQueue mWriteFrameMessageQueue;                // Message queue for the current frame being written.
     otMessageQueue mMessageQueue[kNumPrios];               // Main message queues.
-    otMessage *    mReadMessage;                           // Current Message in the frame being read.
+    otMessage     *mReadMessage;                           // Current Message in the frame being read.
     uint16_t       mReadMessageOffset;                     // Offset within current message being read.
     uint8_t        mMessageBuffer[kMessageReadBufferSize]; // Buffer to hold part of current message being read.
-    uint8_t *      mReadMessageTail;                       // Pointer to end of current part in mMessageBuffer.
+    uint8_t       *mReadMessageTail;                       // Pointer to end of current part in mMessageBuffer.
 #endif
 };
 
diff --git a/src/lib/spinel/spinel_encoder.hpp b/src/lib/spinel/spinel_encoder.hpp
index 0790054..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
@@ -683,7 +683,7 @@
         kMaxNestedStructs     = 4,  ///< Maximum number of nested structs.
     };
 
-    Spinel::Buffer &              mNcpBuffer;
+    Spinel::Buffer               &mNcpBuffer;
     Spinel::Buffer::WritePosition mStructPosition[kMaxNestedStructs];
     uint8_t                       mNumOpenStructs;
 
diff --git a/src/lib/spinel/spinel_interface.hpp b/src/lib/spinel/spinel_interface.hpp
index 585f257..78b21dd 100644
--- a/src/lib/spinel/spinel_interface.hpp
+++ b/src/lib/spinel/spinel_interface.hpp
@@ -36,6 +36,8 @@
 #define POSIX_APP_SPINEL_INTERFACE_HPP_
 
 #include "lib/hdlc/hdlc.hpp"
+#include "lib/spinel/spinel.h"
+#include "lib/url/url.hpp"
 
 namespace ot {
 namespace Spinel {
@@ -58,6 +60,107 @@
     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);
+    }
+
+    /**
+     * Initializes the interface to the Radio Co-processor (RCP)
+     *
+     * @note This method should be called before reading and sending spinel frames to the interface.
+     *
+     * @param[in]  aRadioUrl          RadioUrl parsed from radio url.
+     *
+     * @retval OT_ERROR_NONE          The interface is initialized successfully
+     * @retval OT_ERROR_ALREADY       The interface is already initialized.
+     * @retval OT_ERROR_INVALID_ARGS  The UART device or executable cannot be found or failed to open/run.
+     *
+     */
+    virtual otError Init(const Url::Url &aRadioUrl) = 0;
+
+    /**
+     * Deinitializes the interface to the RCP.
+     *
+     */
+    virtual void Deinit(void) = 0;
+
+    /**
+     * Encodes and sends a spinel frame to Radio Co-processor (RCP) over the socket.
+     *
+     * @param[in] aFrame     A pointer to buffer containing the spinel frame to send.
+     * @param[in] aLength    The length (number of bytes) in the frame.
+     *
+     * @retval OT_ERROR_NONE     Successfully encoded and sent the spinel frame.
+     * @retval OT_ERROR_BUSY     Failed due to another operation is on going.
+     * @retval OT_ERROR_NO_BUFS  Insufficient buffer space available to encode the frame.
+     * @retval OT_ERROR_FAILED   Failed to call the SPI driver to send the frame.
+     *
+     */
+    virtual otError SendFrame(const uint8_t *aFrame, uint16_t aLength) = 0;
+
+    /**
+     * Waits for receiving part or all of spinel frame within specified interval.
+     *
+     * @param[in]  aTimeout  The timeout value in microseconds.
+     *
+     * @retval OT_ERROR_NONE             Part or all of spinel frame is received.
+     * @retval OT_ERROR_RESPONSE_TIMEOUT No spinel frame is received within @p aTimeout.
+     *
+     */
+    virtual otError WaitForFrame(uint64_t aTimeoutUs) = 0;
+
+    /**
+     * Updates the file descriptor sets with file descriptors used by the radio driver.
+     *
+     * @param[in,out]   aMainloopContext  A pointer to the mainloop context.
+     *
+     */
+    virtual void UpdateFdSet(void *aMainloopContext) = 0;
+
+    /**
+     * Performs radio driver processing.
+     *
+     * @param[in]   aMainloopContext  A pointer to the mainloop context.
+     *
+     */
+    virtual void Process(const void *aMainloopContext) = 0;
+
+    /**
+     * Returns the bus speed between the host and the radio.
+     *
+     * @returns   Bus speed in bits/second.
+     *
+     */
+    virtual uint32_t GetBusSpeed(void) const = 0;
+
+    /**
+     * Hardware resets the RCP.
+     *
+     * @retval OT_ERROR_NONE            Successfully reset the RCP.
+     * @retval OT_ERROR_NOT_IMPLEMENT   The hardware reset is not implemented.
+     *
+     */
+    virtual otError HardwareReset(void) = 0;
+
+    /**
+     * Marks destructor virtual method.
+     *
+     */
+    virtual ~SpinelInterface() = default;
 };
 } // namespace Spinel
 } // namespace ot
diff --git a/src/lib/url/CMakeLists.txt b/src/lib/url/CMakeLists.txt
index ed74f5c..0ed677b 100644
--- a/src/lib/url/CMakeLists.txt
+++ b/src/lib/url/CMakeLists.txt
@@ -31,3 +31,16 @@
 )
 
 target_link_libraries(openthread-url PRIVATE ot-config)
+
+if(BUILD_TESTING)
+    add_executable(ot-test-url
+        url.cpp
+    )
+    target_compile_definitions(ot-test-url
+        PRIVATE -DSELF_TEST=1
+    )
+    target_link_libraries(ot-test-url
+        PRIVATE ot-config
+    )
+    add_test(NAME ot-test-url COMMAND ot-test-url)
+endif()
diff --git a/src/lib/url/Makefile.am b/src/lib/url/Makefile.am
index 11037e2..d90e96e 100644
--- a/src/lib/url/Makefile.am
+++ b/src/lib/url/Makefile.am
@@ -41,21 +41,4 @@
     url.hpp                                   \
     $(NULL)
 
-check_PROGRAMS = test-url
-
-test_url_CPPFLAGS                                             = \
-    -I$(top_srcdir)/include                                     \
-    -I$(top_srcdir)/src                                         \
-    -I$(top_srcdir)/src/core                                    \
-    -DSELF_TEST                                                 \
-    $(NULL)
-
-test_url_SOURCES                    = \
-    url.cpp                           \
-    $(NULL)
-
-TESTS                               = \
-    test-url                          \
-    $(NULL)
-
 include $(abs_top_nlbuild_autotools_dir)/automake/post.am
diff --git a/src/lib/url/url.cpp b/src/lib/url/url.cpp
index 7d4312c..ed576d2 100644
--- a/src/lib/url/url.cpp
+++ b/src/lib/url/url.cpp
@@ -40,7 +40,7 @@
 otError Url::Init(char *aUrl)
 {
     otError error = OT_ERROR_NONE;
-    char *  url   = aUrl;
+    char   *url   = aUrl;
 
     mEnd      = aUrl + strlen(aUrl);
     mProtocol = aUrl;
@@ -73,9 +73,9 @@
 
 const char *Url::GetValue(const char *aName, const char *aLastValue) const
 {
-    const char * rval = nullptr;
+    const char  *rval = nullptr;
     const size_t len  = strlen(aName);
-    const char * start;
+    const char  *start;
 
     if (aLastValue == nullptr)
     {
@@ -158,7 +158,7 @@
 {
     char         url[] = "spinel:///dev/ttyUSB0?rtscts&baudrate=115200&verbose&verbose&verbose";
     ot::Url::Url args;
-    const char * arg = nullptr;
+    const char  *arg = nullptr;
 
     assert(!args.Init(url));
     assert(!strcmp(args.GetPath(), "/dev/ttyUSB0"));
@@ -188,7 +188,7 @@
 {
     char         url[] = "spinel+exec:///path/to/ot-rcp?arg=1&arg=arg2&arg=3";
     ot::Url::Url args;
-    const char * arg = nullptr;
+    const char  *arg = nullptr;
 
     assert(!args.Init(url));
     assert(!strcmp(args.GetPath(), "/path/to/ot-rcp"));
diff --git a/src/ncp/changed_props_set.cpp b/src/ncp/changed_props_set.cpp
index 3299c32..ceac025 100644
--- a/src/ncp/changed_props_set.cpp
+++ b/src/ncp/changed_props_set.cpp
@@ -68,10 +68,6 @@
 #if OPENTHREAD_CONFIG_JAM_DETECTION_ENABLE
     {SPINEL_PROP_JAM_DETECTED, SPINEL_STATUS_OK, true},
 #endif
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-    {SPINEL_PROP_NEST_LEGACY_ULA_PREFIX, SPINEL_STATUS_OK, true},
-    {SPINEL_PROP_NEST_LEGACY_LAST_NODE_JOINED, SPINEL_STATUS_OK, true},
-#endif
     {SPINEL_PROP_LAST_STATUS, SPINEL_STATUS_JOIN_FAILURE, false},
     {SPINEL_PROP_MAC_SCAN_STATE, SPINEL_STATUS_OK, false},
     {SPINEL_PROP_IPV6_MULTICAST_ADDRESS_TABLE, SPINEL_STATUS_OK, true},
diff --git a/src/ncp/example_vendor_hook.cpp b/src/ncp/example_vendor_hook.cpp
index 4c0b72d..62c0b6a 100644
--- a/src/ncp/example_vendor_hook.cpp
+++ b/src/ncp/example_vendor_hook.cpp
@@ -150,7 +150,7 @@
 extern "C" void otAppNcpInit(otInstance *aInstance)
 {
     NcpVendorUart *ncpVendor = nullptr;
-    ot::Instance * instance  = static_cast<ot::Instance *>(aInstance);
+    ot::Instance  *instance  = static_cast<ot::Instance *>(aInstance);
 
     ncpVendor = new (&sNcpVendorRaw) NcpVendorUart(instance);
 
diff --git a/src/ncp/ftd.cmake b/src/ncp/ftd.cmake
index 4ba3130..43b5f4f 100644
--- a/src/ncp/ftd.cmake
+++ b/src/ncp/ftd.cmake
@@ -49,5 +49,6 @@
         ${OT_MBEDTLS}
         openthread-hdlc
         openthread-spinel-ncp
+        ot-config-ftd
         ot-config
 )
diff --git a/src/ncp/mtd.cmake b/src/ncp/mtd.cmake
index e1ceb6f..6d8de8e 100644
--- a/src/ncp/mtd.cmake
+++ b/src/ncp/mtd.cmake
@@ -49,5 +49,6 @@
         ${OT_MBEDTLS}
         openthread-hdlc
         openthread-spinel-ncp
+        ot-config-mtd
         ot-config
 )
diff --git a/src/ncp/ncp_base.cpp b/src/ncp/ncp_base.cpp
index 0f922a3..73541ae 100644
--- a/src/ncp/ncp_base.cpp
+++ b/src/ncp/ncp_base.cpp
@@ -56,10 +56,7 @@
 // ----------------------------------------------------------------------------
 
 #if OPENTHREAD_RADIO || OPENTHREAD_CONFIG_LINK_RAW_ENABLE
-static bool HasOnly1BitSet(uint32_t aValue)
-{
-    return aValue != 0 && ((aValue & (aValue - 1)) == 0);
-}
+static bool HasOnly1BitSet(uint32_t aValue) { return aValue != 0 && ((aValue & (aValue - 1)) == 0); }
 
 static uint8_t IndexOfMSB(uint32_t aValue)
 {
@@ -288,17 +285,13 @@
 #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);
 #endif
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-    mLegacyNodeDidJoin = false;
-    mLegacyHandlers    = nullptr;
-    memset(mLegacyUlaPrefix, 0, sizeof(mLegacyUlaPrefix));
-    memset(&mLegacyLastJoinedNode, 0, sizeof(mLegacyLastJoinedNode));
-#endif
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
     mChangedPropsSet.AddLastStatus(SPINEL_STATUS_RESET_UNKNOWN);
     mUpdateChangedPropsTask.Post();
@@ -308,10 +301,7 @@
 #endif
 }
 
-NcpBase *NcpBase::GetNcpInstance(void)
-{
-    return sNcpInstance;
-}
+NcpBase *NcpBase::GetNcpInstance(void) { return sNcpInstance; }
 
 void NcpBase::ResetCounters(void)
 {
@@ -334,10 +324,7 @@
 // MARK: Serial Traffic Glue
 // ----------------------------------------------------------------------------
 
-Spinel::Buffer::FrameTag NcpBase::GetLastOutboundFrameTag(void)
-{
-    return mTxFrameBuffer.InFrameGetLastTag();
-}
+Spinel::Buffer::FrameTag NcpBase::GetLastOutboundFrameTag(void) { return mTxFrameBuffer.InFrameGetLastTag(); }
 
 void NcpBase::HandleReceive(const uint8_t *aBuf, uint16_t aBufLength)
 {
@@ -402,10 +389,10 @@
     mDisableStreamWrite = false;
 }
 
-void NcpBase::HandleFrameRemovedFromNcpBuffer(void *                   aContext,
+void NcpBase::HandleFrameRemovedFromNcpBuffer(void                    *aContext,
                                               Spinel::Buffer::FrameTag aFrameTag,
                                               Spinel::Buffer::Priority aPriority,
-                                              Spinel::Buffer *         aNcpBuffer)
+                                              Spinel::Buffer          *aNcpBuffer)
 {
     OT_UNUSED_VARIABLE(aNcpBuffer);
     OT_UNUSED_VARIABLE(aPriority);
@@ -477,10 +464,7 @@
     return (mHostPowerState == SPINEL_HOST_POWER_STATE_DEEP_SLEEP && !mHostPowerStateInProgress);
 }
 
-void NcpBase::IncrementFrameErrorCounter(void)
-{
-    mFramingErrorCounter++;
-}
+void NcpBase::IncrementFrameErrorCounter(void) { mFramingErrorCounter++; }
 
 otError NcpBase::StreamWrite(int aStreamId, const uint8_t *aDataPtr, int aDataLen)
 {
@@ -689,7 +673,7 @@
 
 #if OPENTHREAD_CONFIG_NCP_ENABLE_PEEK_POKE
 
-void NcpBase::RegisterPeekPokeDelagates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
+void NcpBase::RegisterPeekPokeDelegates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
                                         otNcpDelegateAllowPeekPoke aAllowPokeDelegate)
 {
     mAllowPeekDelegate = aAllowPeekDelegate;
@@ -1058,7 +1042,7 @@
     otError         error           = OT_ERROR_NONE;
     PropertyHandler handler         = nullptr;
     unsigned int    responseCommand = 0;
-    const uint8_t * valuePtr;
+    const uint8_t  *valuePtr;
     uint16_t        valueLen;
 
     switch (aCommand)
@@ -1075,7 +1059,6 @@
 
     default:
         OT_ASSERT(false);
-        OT_UNREACHABLE_CODE(break);
     }
 
     VerifyOrExit(handler != nullptr, error = PrepareLastStatusResponse(aHeader, SPINEL_STATUS_PROP_NOT_FOUND));
@@ -1182,7 +1165,7 @@
 otError NcpBase::WritePropertyValueInsertedRemovedFrame(uint8_t           aHeader,
                                                         unsigned int      aResponseCommand,
                                                         spinel_prop_key_t aPropKey,
-                                                        const uint8_t *   aValuePtr,
+                                                        const uint8_t    *aValuePtr,
                                                         uint16_t          aValueLen)
 {
     otError error = OT_ERROR_NONE;
@@ -1199,10 +1182,7 @@
 // MARK: Individual Command Handlers
 // ----------------------------------------------------------------------------
 
-otError NcpBase::CommandHandler_NOOP(uint8_t aHeader)
-{
-    return PrepareLastStatusResponse(aHeader, SPINEL_STATUS_OK);
-}
+otError NcpBase::CommandHandler_NOOP(uint8_t aHeader) { return PrepareLastStatusResponse(aHeader, SPINEL_STATUS_OK); }
 
 otError NcpBase::CommandHandler_RESET(uint8_t aHeader)
 {
@@ -1371,7 +1351,7 @@
     }
 #endif
 
-    otDiagProcessCmdLine(mInstance, string, output, sizeof(output));
+    SuccessOrExit(error = otDiagProcessCmdLine(mInstance, string, output, sizeof(output)));
 
     // Prepare the response
     SuccessOrExit(error = mEncoder.BeginFrame(aHeader, SPINEL_CMD_PROP_VALUE_IS, SPINEL_PROP_NEST_STREAM_MFG));
@@ -1860,6 +1840,7 @@
 
 #if OPENTHREAD_RADIO
     SuccessOrExit(error = mEncoder.WriteUintPacked(SPINEL_CAP_RCP_API_VERSION));
+    SuccessOrExit(error = mEncoder.WriteUintPacked(SPINEL_CAP_RCP_MIN_HOST_API_VERSION));
 #endif
 
 #if OPENTHREAD_PLATFORM_POSIX
@@ -1888,9 +1869,7 @@
     SuccessOrExit(error = mEncoder.WriteUintPacked(SPINEL_CAP_JAM_DETECT));
 #endif
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
     SuccessOrExit(error = mEncoder.WriteUintPacked(SPINEL_CAP_CHILD_SUPERVISION));
-#endif
 
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
     SuccessOrExit(error = mEncoder.WriteUintPacked(SPINEL_CAP_CHANNEL_MONITOR));
@@ -1932,10 +1911,6 @@
 
     SuccessOrExit(error = mEncoder.WriteUintPacked(SPINEL_CAP_ROLE_SLEEPY));
 
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-    SuccessOrExit(error = mEncoder.WriteUintPacked(SPINEL_CAP_NEST_LEGACY_INTERFACE));
-#endif
-
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_COMMISSIONER_ENABLE
     SuccessOrExit(error = mEncoder.WriteUintPacked(SPINEL_CAP_THREAD_COMMISSIONER));
 #endif
@@ -2063,7 +2038,6 @@
         if (otThreadGetDeviceRole(mInstance) != OT_DEVICE_ROLE_DISABLED)
         {
             IgnoreError(otThreadSetEnabled(mInstance, false));
-            StopLegacy();
         }
 
         if (otIp6IsEnabled(mInstance))
@@ -2091,10 +2065,7 @@
     return mEncoder.WriteUint8(SPINEL_POWER_STATE_ONLINE);
 }
 
-template <> otError NcpBase::HandlePropertySet<SPINEL_PROP_POWER_STATE>(void)
-{
-    return OT_ERROR_NOT_IMPLEMENTED;
-}
+template <> otError NcpBase::HandlePropertySet<SPINEL_PROP_POWER_STATE>(void) { return OT_ERROR_NOT_IMPLEMENTED; }
 
 template <> otError NcpBase::HandlePropertyGet<SPINEL_PROP_HWADDR>(void)
 {
@@ -2445,7 +2416,6 @@
 
     default:
         ExitNow(error = OT_ERROR_INVALID_ARGS);
-        OT_UNREACHABLE_CODE(break);
     }
 
     IgnoreError(otLoggingSetLevel(logLevel));
@@ -2586,6 +2556,44 @@
 }
 #endif
 
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+template <> otError NcpBase::HandlePropertySet<SPINEL_PROP_PHY_CHAN_TARGET_POWER>(void)
+{
+    otError error;
+    uint8_t channel;
+    int16_t targetPower;
+
+    SuccessOrExit(error = mDecoder.ReadUint8(channel));
+    SuccessOrExit(error = mDecoder.ReadInt16(targetPower));
+    error = otPlatRadioSetChannelTargetPower(mInstance, channel, targetPower);
+
+exit:
+    return error;
+}
+
+template <> otError NcpBase::HandlePropertyInsert<SPINEL_PROP_PHY_CALIBRATED_POWER>(void)
+{
+    otError        error;
+    uint8_t        channel;
+    int16_t        actualPower;
+    const uint8_t *dataPtr;
+    uint16_t       dataLen;
+
+    SuccessOrExit(error = mDecoder.ReadUint8(channel));
+    SuccessOrExit(error = mDecoder.ReadInt16(actualPower));
+    SuccessOrExit(error = mDecoder.ReadDataWithLen(dataPtr, dataLen));
+    error = otPlatRadioAddCalibratedPower(mInstance, channel, actualPower, dataPtr, dataLen);
+
+exit:
+    return error;
+}
+
+template <> otError NcpBase::HandlePropertySet<SPINEL_PROP_PHY_CALIBRATED_POWER>(void)
+{
+    return otPlatRadioClearCalibratedPowers(mInstance);
+}
+#endif // OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+
 } // namespace Ncp
 } // namespace ot
 
@@ -2594,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->RegisterPeekPoke(aAllowPeekDelegate, aAllowPokeDelegate);
     }
 }
 #endif // OPENTHREAD_CONFIG_NCP_ENABLE_PEEK_POKE
diff --git a/src/ncp/ncp_base.hpp b/src/ncp/ncp_base.hpp
index f4e9f64..995dbc4 100644
--- a/src/ncp/ncp_base.hpp
+++ b/src/ncp/ncp_base.hpp
@@ -126,40 +126,10 @@
      * @param[in] aAllowPokeDelegate      Delegate function pointer for poke operation.
      *
      */
-    void RegisterPeekPokeDelagates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
+    void RegisterPeekPokeDelegates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
                                    otNcpDelegateAllowPeekPoke aAllowPokeDelegate);
 #endif
 
-#if OPENTHREAD_MTD || OPENTHREAD_FTD
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-    /**
-     * This callback is invoked by the legacy stack to notify that a new
-     * legacy node did join the network.
-     *
-     * @param[in]   aExtAddr    The extended address of the joined node.
-     *
-     */
-    void HandleLegacyNodeDidJoin(const otExtAddress *aExtAddr);
-
-    /**
-     * This callback is invoked by the legacy stack to notify that the
-     * legacy ULA prefix has changed.
-     *
-     * param[in]    aUlaPrefix  The changed ULA prefix.
-     *
-     */
-    void HandleDidReceiveNewLegacyUlaPrefix(const uint8_t *aUlaPrefix);
-
-    /**
-     * This method registers a set of legacy handlers with NCP.
-     *
-     * @param[in] aHandlers    A pointer to a handler struct.
-     *
-     */
-    void RegisterLegacyHandlers(const otNcpLegacyHandlers *aHandlers);
-#endif
-#endif // OPENTHREAD_MTD || OPENTHREAD_FTD
-
     /**
      * This method is called by the framer whenever a framing error is detected.
      */
@@ -237,7 +207,7 @@
     otError WritePropertyValueInsertedRemovedFrame(uint8_t           aHeader,
                                                    unsigned int      aResponseCommand,
                                                    spinel_prop_key_t aPropKey,
-                                                   const uint8_t *   aValuePtr,
+                                                   const uint8_t    *aValuePtr,
                                                    uint16_t          aValueLen);
 
     otError SendQueuedResponses(void);
@@ -262,10 +232,10 @@
     static void UpdateChangedProps(Tasklet &aTasklet);
     void        UpdateChangedProps(void);
 
-    static void HandleFrameRemovedFromNcpBuffer(void *                   aContext,
+    static void HandleFrameRemovedFromNcpBuffer(void                    *aContext,
                                                 Spinel::Buffer::FrameTag aFrameTag,
                                                 Spinel::Buffer::Priority aPriority,
-                                                Spinel::Buffer *         aNcpBuffer);
+                                                Spinel::Buffer          *aNcpBuffer);
     void        HandleFrameRemovedFromNcpBuffer(Spinel::Buffer::FrameTag aFrameTag);
 
     otError EncodeChannelMask(uint32_t aChannelMask);
@@ -277,7 +247,7 @@
     static void LinkRawReceiveDone(otInstance *aInstance, otRadioFrame *aFrame, otError aError);
     void        LinkRawReceiveDone(otRadioFrame *aFrame, otError aError);
 
-    static void LinkRawTransmitDone(otInstance *  aInstance,
+    static void LinkRawTransmitDone(otInstance   *aInstance,
                                     otRadioFrame *aFrame,
                                     otRadioFrame *aAckFrame,
                                     otError       aError);
@@ -302,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);
@@ -325,7 +297,7 @@
     static void HandleCommissionerEnergyReport_Jump(uint32_t       aChannelMask,
                                                     const uint8_t *aEnergyData,
                                                     uint8_t        aLength,
-                                                    void *         aContext);
+                                                    void          *aContext);
     void        HandleCommissionerEnergyReport(uint32_t aChannelMask, const uint8_t *aEnergyData, uint8_t aLength);
 
     static void HandleCommissionerPanIdConflict_Jump(uint16_t aPanId, uint32_t aChannelMask, void *aContext);
@@ -338,12 +310,12 @@
 #endif
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
-    static void HandleLinkMetricsReport_Jump(const otIp6Address *       aSource,
+    static void HandleLinkMetricsReport_Jump(const otIp6Address        *aSource,
                                              const otLinkMetricsValues *aMetricsValues,
                                              uint8_t                    aStatus,
-                                             void *                     aContext);
+                                             void                      *aContext);
 
-    void HandleLinkMetricsReport(const otIp6Address *       aSource,
+    void HandleLinkMetricsReport(const otIp6Address        *aSource,
                                  const otLinkMetricsValues *aMetricsValues,
                                  uint8_t                    aStatus);
 
@@ -352,16 +324,16 @@
     void HandleLinkMetricsMgmtResponse(const otIp6Address *aSource, uint8_t aStatus);
 
     static void HandleLinkMetricsEnhAckProbingIeReport_Jump(otShortAddress             aShortAddress,
-                                                            const otExtAddress *       aExtAddress,
+                                                            const otExtAddress        *aExtAddress,
                                                             const otLinkMetricsValues *aMetricsValues,
-                                                            void *                     aContext);
+                                                            void                      *aContext);
 
     void HandleLinkMetricsEnhAckProbingIeReport(otShortAddress             aShortAddress,
-                                                const otExtAddress *       aExtAddress,
+                                                const otExtAddress        *aExtAddress,
                                                 const otLinkMetricsValues *aMetricsValues);
 #endif
 
-    static void HandleMlrRegResult_Jump(void *              aContext,
+    static void HandleMlrRegResult_Jump(void               *aContext,
                                         otError             aError,
                                         uint8_t             aMlrStatus,
                                         const otIp6Address *aFailedAddresses,
@@ -374,9 +346,9 @@
     otError EncodeOperationalDataset(const otOperationalDataset &aDataset);
 
     otError DecodeOperationalDataset(otOperationalDataset &aDataset,
-                                     const uint8_t **      aTlvs             = nullptr,
-                                     uint8_t *             aTlvsLength       = nullptr,
-                                     const otIp6Address ** aDestIpAddress    = nullptr,
+                                     const uint8_t       **aTlvs             = nullptr,
+                                     uint8_t              *aTlvsLength       = nullptr,
+                                     const otIp6Address  **aDestIpAddress    = nullptr,
                                      bool                  aAllowEmptyValues = false);
 
     otError EncodeNeighborInfo(const otNeighborInfo &aNeighborInfo);
@@ -393,11 +365,11 @@
 #endif
 
 #if OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE
-    static void HandleUdpForwardStream(otMessage *   aMessage,
+    static void HandleUdpForwardStream(otMessage    *aMessage,
                                        uint16_t      aPeerPort,
                                        otIp6Address *aPeerAddr,
                                        uint16_t      aSockPort,
-                                       void *        aContext);
+                                       void         *aContext);
     void HandleUdpForwardStream(otMessage *aMessage, uint16_t aPeerPort, otIp6Address &aPeerAddr, uint16_t aPort);
 #endif // OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
@@ -485,14 +457,6 @@
 
     void ResetCounters(void);
 
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-    void StartLegacy(void);
-    void StopLegacy(void);
-#else
-    void StartLegacy(void) {}
-    void StopLegacy(void) {}
-#endif
-
     static uint8_t      ConvertLogLevel(otLogLevel aLogLevel);
     static unsigned int ConvertLogRegion(otLogRegion aLogRegion);
 
@@ -563,10 +527,10 @@
 #endif // OPENTHREAD_ENABLE_NCP_VENDOR_HOOK
 
 protected:
-    static NcpBase *       sNcpInstance;
+    static NcpBase        *sNcpInstance;
     static spinel_status_t ThreadErrorToSpinelStatus(otError aError);
     static uint8_t         LinkFlagsToFlagByte(bool aRxOnWhenIdle, bool aDeviceType, bool aNetworkData);
-    Instance *             mInstance;
+    Instance              *mInstance;
     Spinel::Buffer         mTxFrameBuffer;
     Spinel::Encoder        mEncoder;
     Spinel::Decoder        mDecoder;
@@ -651,23 +615,17 @@
 
     static void HandleSrpClientCallback(otError                    aError,
                                         const otSrpClientHostInfo *aHostInfo,
-                                        const otSrpClientService * aServices,
-                                        const otSrpClientService * aRemovedServices,
-                                        void *                     aContext);
+                                        const otSrpClientService  *aServices,
+                                        const otSrpClientService  *aRemovedServices,
+                                        void                      *aContext);
     void        HandleSrpClientCallback(otError                    aError,
                                         const otSrpClientHostInfo *aHostInfo,
-                                        const otSrpClientService * aServices,
-                                        const otSrpClientService * aRemovedServices);
+                                        const otSrpClientService  *aServices,
+                                        const otSrpClientService  *aRemovedServices);
 
     bool mSrpClientCallbackEnabled;
 #endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
 
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-    const otNcpLegacyHandlers *mLegacyHandlers;
-    uint8_t                    mLegacyUlaPrefix[OT_NCP_LEGACY_ULA_PREFIX_LENGTH];
-    otExtAddress               mLegacyLastJoinedNode;
-    bool                       mLegacyNodeDidJoin;
-#endif
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
 
     uint32_t mFramingErrorCounter;          // Number of improperly formed received spinel frames.
diff --git a/src/ncp/ncp_base_dispatcher.cpp b/src/ncp/ncp_base_dispatcher.cpp
index 89b1f61..6fc4267 100644
--- a/src/ncp/ncp_base_dispatcher.cpp
+++ b/src/ncp/ncp_base_dispatcher.cpp
@@ -153,6 +153,7 @@
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
 #if OPENTHREAD_RADIO
         OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_RCP_API_VERSION),
+        OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_RCP_MIN_HOST_API_VERSION),
 #endif
 #if OPENTHREAD_MTD || OPENTHREAD_FTD
         OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_CNTR_TX_PKT_TOTAL),
@@ -343,10 +344,8 @@
         OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_TIME_SYNC_PERIOD),
         OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_TIME_SYNC_XTAL_THRESHOLD),
 #endif
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
         OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_CHILD_SUPERVISION_INTERVAL),
         OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_CHILD_SUPERVISION_CHECK_TIMEOUT),
-#endif
 #endif // OPENTHREAD_FTD
 #if OPENTHREAD_PLATFORM_POSIX
         OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_RCP_VERSION),
@@ -370,10 +369,6 @@
 #endif
 #endif
 
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-        OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_NEST_LEGACY_ULA_PREFIX),
-        OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_NEST_LEGACY_LAST_NODE_JOINED),
-#endif
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
         OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_DEBUG_TEST_ASSERT),
         OT_NCP_GET_HANDLER_ENTRY(SPINEL_PROP_DEBUG_NCP_LOG_LEVEL),
@@ -419,6 +414,10 @@
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_PHY_FEM_LNA_GAIN),
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_PHY_CHAN_MAX_POWER),
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_PHY_REGION_CODE),
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+        OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_PHY_CALIBRATED_POWER),
+        OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_PHY_CHAN_TARGET_POWER),
+#endif
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_MAC_SCAN_STATE),
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_MAC_SCAN_MASK),
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_MAC_SCAN_PERIOD),
@@ -502,7 +501,6 @@
 #if OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_RADIO_COEX_ENABLE),
 #endif
-
 #if OPENTHREAD_MTD || OPENTHREAD_FTD
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_MAC_ALLOWLIST),
@@ -606,10 +604,8 @@
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_TIME_SYNC_PERIOD),
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_TIME_SYNC_XTAL_THRESHOLD),
 #endif
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_CHILD_SUPERVISION_INTERVAL),
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_CHILD_SUPERVISION_CHECK_TIMEOUT),
-#endif
 #endif // OPENTHREAD_FTD
 #if OPENTHREAD_CONFIG_IP6_SLAAC_ENABLE
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_SLAAC_ENABLED),
@@ -626,9 +622,6 @@
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_SRP_CLIENT_SERVICE_KEY_ENABLED),
 #endif
 #endif
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-        OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_NEST_LEGACY_ULA_PREFIX),
-#endif
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
 #if OPENTHREAD_CONFIG_LOG_LEVEL_DYNAMIC_ENABLE
         OT_NCP_SET_HANDLER_ENTRY(SPINEL_PROP_DEBUG_NCP_LOG_LEVEL),
@@ -655,6 +648,9 @@
     }
 
     constexpr static HandlerEntry sHandlerEntries[] = {
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+        OT_NCP_INSERT_HANDLER_ENTRY(SPINEL_PROP_PHY_CALIBRATED_POWER),
+#endif
 #if OPENTHREAD_MTD || OPENTHREAD_FTD
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
         OT_NCP_INSERT_HANDLER_ENTRY(SPINEL_PROP_THREAD_ON_MESH_NETS),
diff --git a/src/ncp/ncp_base_ftd.cpp b/src/ncp/ncp_base_ftd.cpp
index a7fbdb0..fb763c4 100644
--- a/src/ncp/ncp_base_ftd.cpp
+++ b/src/ncp/ncp_base_ftd.cpp
@@ -39,9 +39,7 @@
 #if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE
 #include <openthread/channel_manager.h>
 #endif
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
 #include <openthread/child_supervision.h>
-#endif
 #include <openthread/dataset.h>
 #include <openthread/dataset_ftd.h>
 #include <openthread/diag.h>
@@ -87,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);
@@ -118,6 +117,7 @@
 exit:
     return;
 }
+#endif
 
 void NcpBase::HandleNeighborTableChanged(otNeighborTableEvent aEvent, const otNeighborTableEntryInfo *aEntry)
 {
@@ -366,9 +366,9 @@
     }
     else
     {
-        for (size_t i = 0; i < sizeof(otIp6InterfaceIdentifier); i++)
+        for (uint8_t i : iid->mFields.m8)
         {
-            SuccessOrExit(error = mEncoder.WriteUint8(iid->mFields.m8[i]));
+            SuccessOrExit(error = mEncoder.WriteUint8(i));
         }
     }
 
@@ -388,9 +388,9 @@
     {
         otIp6InterfaceIdentifier iid;
 
-        for (size_t i = 0; i < sizeof(otIp6InterfaceIdentifier); i++)
+        for (uint8_t &i : iid.mFields.m8)
         {
-            SuccessOrExit(error = mDecoder.ReadUint8(iid.mFields.m8[i]));
+            SuccessOrExit(error = mDecoder.ReadUint8(i));
         }
 
         SuccessOrExit(error = otThreadSetFixedDuaInterfaceIdentifier(mInstance, &iid));
@@ -763,7 +763,7 @@
     bool                withDiscerner = false;
     const otExtAddress *eui64;
     uint32_t            timeout;
-    const char *        psk;
+    const char         *psk;
 
     SuccessOrExit(error = mDecoder.OpenStruct());
 
@@ -912,7 +912,7 @@
 void NcpBase::HandleCommissionerEnergyReport_Jump(uint32_t       aChannelMask,
                                                   const uint8_t *aEnergyData,
                                                   uint8_t        aLength,
-                                                  void *         aContext)
+                                                  void          *aContext)
 {
     static_cast<NcpBase *>(aContext)->HandleCommissionerEnergyReport(aChannelMask, aEnergyData, aLength);
 }
@@ -997,7 +997,7 @@
 template <> otError NcpBase::HandlePropertySet<SPINEL_PROP_MESHCOP_COMMISSIONER_MGMT_SET>(void)
 {
     otError                error = OT_ERROR_NONE;
-    const uint8_t *        tlvs;
+    const uint8_t         *tlvs;
     uint16_t               length;
     otCommissioningDataset dataset;
 
@@ -1014,8 +1014,8 @@
 otError NcpBase::HandlePropertySet_SPINEL_PROP_MESHCOP_COMMISSIONER_GENERATE_PSKC(uint8_t aHeader)
 {
     otError        error = OT_ERROR_NONE;
-    const char *   passPhrase;
-    const char *   networkName;
+    const char    *passPhrase;
+    const char    *networkName;
     const uint8_t *extPanIdData;
     uint16_t       length;
     otPskc         pskc;
@@ -1072,7 +1072,7 @@
 {
     otError             error         = OT_ERROR_NONE;
     const otExtAddress *eui64         = nullptr;
-    const char *        pskd          = nullptr;
+    const char         *pskd          = nullptr;
     uint32_t            joinerTimeout = 0;
 
     SuccessOrExit(error = mDecoder.ReadUtf8(pskd));
@@ -1223,8 +1223,6 @@
     return error;
 }
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-
 template <> otError NcpBase::HandlePropertyGet<SPINEL_PROP_CHILD_SUPERVISION_INTERVAL>(void)
 {
     return mEncoder.WriteUint16(otChildSupervisionGetInterval(mInstance));
@@ -1242,8 +1240,6 @@
     return error;
 }
 
-#endif // OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-
 #if OPENTHREAD_CONFIG_CHANNEL_MANAGER_ENABLE
 
 template <> otError NcpBase::HandlePropertyGet<SPINEL_PROP_CHANNEL_MANAGER_NEW_CHANNEL>(void)
diff --git a/src/ncp/ncp_base_mtd.cpp b/src/ncp/ncp_base_mtd.cpp
index e2dbb53..48a9270 100644
--- a/src/ncp/ncp_base_mtd.cpp
+++ b/src/ncp/ncp_base_mtd.cpp
@@ -40,9 +40,7 @@
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
 #include <openthread/channel_monitor.h>
 #endif
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
 #include <openthread/child_supervision.h>
-#endif
 #include <openthread/diag.h>
 #include <openthread/icmp6.h>
 #if OPENTHREAD_CONFIG_JAM_DETECTION_ENABLE
@@ -365,7 +363,7 @@
     return error;
 }
 
-void NcpBase::HandleMlrRegResult_Jump(void *              aContext,
+void NcpBase::HandleMlrRegResult_Jump(void               *aContext,
                                       otError             aError,
                                       uint8_t             aMlrStatus,
                                       const otIp6Address *aFailedAddresses,
@@ -520,12 +518,10 @@
         if (enabled)
         {
             error = otThreadSetEnabled(mInstance, true);
-            StartLegacy();
         }
         else
         {
             error = otThreadSetEnabled(mInstance, false);
-            StopLegacy();
         }
     }
 
@@ -806,6 +802,11 @@
             SuccessOrExit(error = mEncoder.WriteInt8(lastRssi));
             SuccessOrExit(error = mEncoder.WriteUint8(parentInfo.mLinkQualityIn));
             SuccessOrExit(error = mEncoder.WriteUint8(parentInfo.mLinkQualityOut));
+            SuccessOrExit(error = mEncoder.WriteUint8(parentInfo.mVersion));
+#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
+            SuccessOrExit(error = mEncoder.WriteUint8(parentInfo.mCslClockAccuracy));
+            SuccessOrExit(error = mEncoder.WriteUint8(parentInfo.mCslUncertainty));
+#endif
         }
         else
         {
@@ -1094,7 +1095,7 @@
     otError         error = OT_ERROR_NONE;
     otServiceConfig cfg;
     bool            stable;
-    const uint8_t * data;
+    const uint8_t  *data;
     uint16_t        dataLen;
 
     VerifyOrExit(mAllowLocalServerDataChange, error = OT_ERROR_INVALID_STATE);
@@ -1367,9 +1368,9 @@
 }
 
 otError NcpBase::DecodeOperationalDataset(otOperationalDataset &aDataset,
-                                          const uint8_t **      aTlvs,
-                                          uint8_t *             aTlvsLength,
-                                          const otIp6Address ** aDestIpAddress,
+                                          const uint8_t       **aTlvs,
+                                          uint8_t              *aTlvsLength,
+                                          const otIp6Address  **aDestIpAddress,
                                           bool                  aAllowEmptyValues)
 {
     otError error = OT_ERROR_NONE;
@@ -1651,7 +1652,7 @@
 {
     otError              error = OT_ERROR_NONE;
     otOperationalDataset dataset;
-    const uint8_t *      extraTlvs;
+    const uint8_t       *extraTlvs;
     uint8_t              extraTlvsLength;
 
     SuccessOrExit(error = DecodeOperationalDataset(dataset, &extraTlvs, &extraTlvsLength));
@@ -1666,7 +1667,7 @@
 {
     otError              error = OT_ERROR_NONE;
     otOperationalDataset dataset;
-    const uint8_t *      extraTlvs;
+    const uint8_t       *extraTlvs;
     uint8_t              extraTlvsLength;
 
     SuccessOrExit(error = DecodeOperationalDataset(dataset, &extraTlvs, &extraTlvsLength));
@@ -1681,9 +1682,9 @@
 {
     otError              error = OT_ERROR_NONE;
     otOperationalDataset dataset;
-    const uint8_t *      extraTlvs;
+    const uint8_t       *extraTlvs;
     uint8_t              extraTlvsLength;
-    const otIp6Address * destIpAddress;
+    const otIp6Address  *destIpAddress;
 
     SuccessOrExit(error = DecodeOperationalDataset(dataset, &extraTlvs, &extraTlvsLength, &destIpAddress, true));
     error = otDatasetSendMgmtActiveGet(mInstance, &dataset.mComponents, extraTlvs, extraTlvsLength, destIpAddress);
@@ -1696,9 +1697,9 @@
 {
     otError              error = OT_ERROR_NONE;
     otOperationalDataset dataset;
-    const uint8_t *      extraTlvs;
+    const uint8_t       *extraTlvs;
     uint8_t              extraTlvsLength;
-    const otIp6Address * destIpAddress;
+    const otIp6Address  *destIpAddress;
 
     SuccessOrExit(error = DecodeOperationalDataset(dataset, &extraTlvs, &extraTlvsLength, &destIpAddress, true));
     error = otDatasetSendMgmtPendingGet(mInstance, &dataset.mComponents, extraTlvs, extraTlvsLength, destIpAddress);
@@ -2253,7 +2254,7 @@
     uint16_t       frameLen = 0;
     const uint8_t *metaPtr  = nullptr;
     uint16_t       metaLen  = 0;
-    otMessage *    message  = nullptr;
+    otMessage     *message  = nullptr;
     otError        error    = OT_ERROR_NONE;
 
     SuccessOrExit(error = mDecoder.ReadDataWithLen(framePtr, frameLen));
@@ -2388,8 +2389,6 @@
 
 #endif // OPENTHREAD_CONFIG_JAM_DETECTION_ENABLE
 
-#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-
 template <> otError NcpBase::HandlePropertyGet<SPINEL_PROP_CHILD_SUPERVISION_CHECK_TIMEOUT>(void)
 {
     return mEncoder.WriteUint16(otChildSupervisionGetCheckTimeout(mInstance));
@@ -2407,8 +2406,6 @@
     return error;
 }
 
-#endif // OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
-
 #if OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE
 
 template <> otError NcpBase::HandlePropertyGet<SPINEL_PROP_CHANNEL_MONITOR_SAMPLE_INTERVAL>(void)
@@ -3286,11 +3283,11 @@
 
 template <> otError NcpBase::HandlePropertySet<SPINEL_PROP_STREAM_NET_INSECURE>(void)
 {
-    const uint8_t *   framePtr    = nullptr;
+    const uint8_t    *framePtr    = nullptr;
     uint16_t          frameLen    = 0;
-    const uint8_t *   metaPtr     = nullptr;
+    const uint8_t    *metaPtr     = nullptr;
     uint16_t          metaLen     = 0;
-    otMessage *       message     = nullptr;
+    otMessage        *message     = nullptr;
     otError           error       = OT_ERROR_NONE;
     otMessageSettings msgSettings = {false, OT_MESSAGE_PRIORITY_NORMAL};
 
@@ -3662,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;
 
@@ -3702,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());
 
@@ -3734,7 +3731,7 @@
     otError     error;
     const char *name;
     uint16_t    size;
-    char *      hostNameBuffer;
+    char       *hostNameBuffer;
 
     SuccessOrExit(error = mDecoder.ReadUtf8(name));
 
@@ -3835,9 +3832,9 @@
 {
     otError                         error = OT_ERROR_NONE;
     otSrpClientBuffersServiceEntry *entry = nullptr;
-    const char *                    serviceName;
-    const char *                    instanceName;
-    char *                          stringBuffer;
+    const char                     *serviceName;
+    const char                     *instanceName;
+    char                           *stringBuffer;
     uint16_t                        size;
 
     entry = otSrpClientBuffersAllocateService(mInstance);
@@ -3872,8 +3869,8 @@
 template <> otError NcpBase::HandlePropertyRemove<SPINEL_PROP_SRP_CLIENT_SERVICES>(void)
 {
     otError                   error = OT_ERROR_NONE;
-    const char *              serviceName;
-    const char *              instanceName;
+    const char               *serviceName;
+    const char               *instanceName;
     bool                      toClear = false;
     const otSrpClientService *service;
 
@@ -3976,17 +3973,17 @@
 
 void NcpBase::HandleSrpClientCallback(otError                    aError,
                                       const otSrpClientHostInfo *aHostInfo,
-                                      const otSrpClientService * aServices,
-                                      const otSrpClientService * aRemovedServices,
-                                      void *                     aContext)
+                                      const otSrpClientService  *aServices,
+                                      const otSrpClientService  *aRemovedServices,
+                                      void                      *aContext)
 {
     static_cast<NcpBase *>(aContext)->HandleSrpClientCallback(aError, aHostInfo, aServices, aRemovedServices);
 }
 
 void NcpBase::HandleSrpClientCallback(otError                    aError,
                                       const otSrpClientHostInfo *aHostInfo,
-                                      const otSrpClientService * aServices,
-                                      const otSrpClientService * aRemovedServices)
+                                      const otSrpClientService  *aServices,
+                                      const otSrpClientService  *aRemovedServices)
 {
     otError                   error = OT_ERROR_NONE;
     const otSrpClientService *service;
@@ -4076,115 +4073,6 @@
 }
 #endif
 
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-
-void NcpBase::RegisterLegacyHandlers(const otNcpLegacyHandlers *aHandlers)
-{
-    mLegacyHandlers = aHandlers;
-    bool isEnabled;
-
-    VerifyOrExit(mLegacyHandlers != nullptr);
-
-    isEnabled = (otThreadGetDeviceRole(mInstance) != OT_DEVICE_ROLE_DISABLED);
-
-    if (isEnabled)
-    {
-        if (mLegacyHandlers->mStartLegacy)
-        {
-            mLegacyHandlers->mStartLegacy();
-        }
-    }
-    else
-    {
-        if (mLegacyHandlers->mStopLegacy)
-        {
-            mLegacyHandlers->mStopLegacy();
-        }
-    }
-
-    if (mLegacyHandlers->mSetLegacyUlaPrefix)
-    {
-        mLegacyHandlers->mSetLegacyUlaPrefix(mLegacyUlaPrefix);
-    }
-
-exit:
-    return;
-}
-
-void NcpBase::HandleDidReceiveNewLegacyUlaPrefix(const uint8_t *aUlaPrefix)
-{
-    memcpy(mLegacyUlaPrefix, aUlaPrefix, OT_NCP_LEGACY_ULA_PREFIX_LENGTH);
-    mChangedPropsSet.AddProperty(SPINEL_PROP_NEST_LEGACY_ULA_PREFIX);
-    mUpdateChangedPropsTask.Post();
-}
-
-void NcpBase::HandleLegacyNodeDidJoin(const otExtAddress *aExtAddr)
-{
-    mLegacyNodeDidJoin    = true;
-    mLegacyLastJoinedNode = *aExtAddr;
-    mChangedPropsSet.AddProperty(SPINEL_PROP_NEST_LEGACY_LAST_NODE_JOINED);
-    mUpdateChangedPropsTask.Post();
-}
-
-template <> otError NcpBase::HandlePropertyGet<SPINEL_PROP_NEST_LEGACY_ULA_PREFIX>(void)
-{
-    return mEncoder.WriteData(mLegacyUlaPrefix, sizeof(mLegacyUlaPrefix));
-}
-
-template <> otError NcpBase::HandlePropertySet<SPINEL_PROP_NEST_LEGACY_ULA_PREFIX>(void)
-{
-    const uint8_t *ptr = nullptr;
-    uint16_t       len;
-    otError        error = OT_ERROR_NONE;
-
-    SuccessOrExit(error = mDecoder.ReadData(ptr, len));
-
-    VerifyOrExit(len <= sizeof(mLegacyUlaPrefix), error = OT_ERROR_PARSE);
-
-    memset(mLegacyUlaPrefix, 0, sizeof(mLegacyUlaPrefix));
-    memcpy(mLegacyUlaPrefix, ptr, len);
-
-    if ((mLegacyHandlers != nullptr) && (mLegacyHandlers->mSetLegacyUlaPrefix != nullptr))
-    {
-        mLegacyHandlers->mSetLegacyUlaPrefix(mLegacyUlaPrefix);
-    }
-
-exit:
-    return error;
-}
-
-template <> otError NcpBase::HandlePropertyGet<SPINEL_PROP_NEST_LEGACY_LAST_NODE_JOINED>(void)
-{
-    if (!mLegacyNodeDidJoin)
-    {
-        memset(&mLegacyLastJoinedNode, 0, sizeof(mLegacyLastJoinedNode));
-    }
-
-    return mEncoder.WriteEui64(mLegacyLastJoinedNode);
-}
-
-void NcpBase::StartLegacy(void)
-{
-    mLegacyNodeDidJoin = false;
-
-    if ((mLegacyHandlers != nullptr) && (mLegacyHandlers->mStartLegacy != nullptr))
-    {
-        mLegacyHandlers->mStartLegacy();
-    }
-}
-
-void NcpBase::StopLegacy(void)
-{
-    mLegacyNodeDidJoin = false;
-
-    if ((mLegacyHandlers != nullptr) && (mLegacyHandlers->mStopLegacy != nullptr))
-    {
-        mLegacyHandlers->mStopLegacy();
-    }
-}
-
-#endif // OPENTHREAD_CONFIG_LEGACY_ENABLE
-
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
 template <> otError NcpBase::HandlePropertyGet<SPINEL_PROP_THREAD_NETWORK_TIME>(void)
 {
@@ -4201,10 +4089,7 @@
     return error;
 }
 
-void NcpBase::HandleTimeSyncUpdate(void *aContext)
-{
-    static_cast<NcpBase *>(aContext)->HandleTimeSyncUpdate();
-}
+void NcpBase::HandleTimeSyncUpdate(void *aContext) { static_cast<NcpBase *>(aContext)->HandleTimeSyncUpdate(); }
 
 void NcpBase::HandleTimeSyncUpdate(void)
 {
@@ -4343,15 +4228,15 @@
 #endif
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
-void NcpBase::HandleLinkMetricsReport_Jump(const otIp6Address *       aSource,
+void NcpBase::HandleLinkMetricsReport_Jump(const otIp6Address        *aSource,
                                            const otLinkMetricsValues *aMetricsValues,
                                            uint8_t                    aStatus,
-                                           void *                     aContext)
+                                           void                      *aContext)
 {
     static_cast<NcpBase *>(aContext)->HandleLinkMetricsReport(aSource, aMetricsValues, aStatus);
 }
 
-void NcpBase::HandleLinkMetricsReport(const otIp6Address *       aSource,
+void NcpBase::HandleLinkMetricsReport(const otIp6Address        *aSource,
                                       const otLinkMetricsValues *aMetricsValues,
                                       uint8_t                    aStatus)
 {
@@ -4388,16 +4273,16 @@
 }
 
 void NcpBase::HandleLinkMetricsEnhAckProbingIeReport_Jump(otShortAddress             aShortAddress,
-                                                          const otExtAddress *       aExtAddress,
+                                                          const otExtAddress        *aExtAddress,
                                                           const otLinkMetricsValues *aMetricsValues,
-                                                          void *                     aContext)
+                                                          void                      *aContext)
 {
     static_cast<NcpBase *>(aContext)->HandleLinkMetricsEnhAckProbingIeReport(aShortAddress, aExtAddress,
                                                                              aMetricsValues);
 }
 
 void NcpBase::HandleLinkMetricsEnhAckProbingIeReport(otShortAddress             aShortAddress,
-                                                     const otExtAddress *       aExtAddress,
+                                                     const otExtAddress        *aExtAddress,
                                                      const otLinkMetricsValues *aMetricsValues)
 {
     SuccessOrExit(mEncoder.BeginFrame(SPINEL_HEADER_FLAG | SPINEL_HEADER_IID_0, SPINEL_CMD_PROP_VALUE_IS,
@@ -4508,12 +4393,12 @@
 #if OPENTHREAD_CONFIG_UDP_FORWARD_ENABLE
 template <> otError NcpBase::HandlePropertySet<SPINEL_PROP_THREAD_UDP_FORWARD_STREAM>(void)
 {
-    const uint8_t *     framePtr = nullptr;
+    const uint8_t      *framePtr = nullptr;
     uint16_t            frameLen = 0;
     const otIp6Address *peerAddr;
     uint16_t            peerPort;
     uint16_t            sockPort;
-    otMessage *         message;
+    otMessage          *message;
     otError             error       = OT_ERROR_NONE;
     otMessageSettings   msgSettings = {false, OT_MESSAGE_PRIORITY_NORMAL};
 
@@ -4543,11 +4428,11 @@
     return error;
 }
 
-void NcpBase::HandleUdpForwardStream(otMessage *   aMessage,
+void NcpBase::HandleUdpForwardStream(otMessage    *aMessage,
                                      uint16_t      aPeerPort,
                                      otIp6Address *aPeerAddr,
                                      uint16_t      aSockPort,
-                                     void *        aContext)
+                                     void         *aContext)
 {
     static_cast<NcpBase *>(aContext)->HandleUdpForwardStream(aMessage, aPeerPort, *aPeerAddr, aSockPort);
 }
@@ -4717,11 +4602,7 @@
                 break;
             }
 
-            if ((otThreadGetDeviceRole(mInstance) == OT_DEVICE_ROLE_LEADER) && otThreadIsSingleton(mInstance)
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-                && !mLegacyNodeDidJoin
-#endif
-            )
+            if ((otThreadGetDeviceRole(mInstance) == OT_DEVICE_ROLE_LEADER) && otThreadIsSingleton(mInstance))
             {
                 mThreadChangedFlags &= ~static_cast<uint32_t>(OT_CHANGED_THREAD_PARTITION_ID);
                 IgnoreError(otThreadSetEnabled(mInstance, false));
@@ -4784,53 +4665,4 @@
 } // namespace Ncp
 } // namespace ot
 
-// ----------------------------------------------------------------------------
-// MARK: Legacy network APIs
-// ----------------------------------------------------------------------------
-
-void otNcpRegisterLegacyHandlers(const otNcpLegacyHandlers *aHandlers)
-{
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-    ot::Ncp::NcpBase *ncp = ot::Ncp::NcpBase::GetNcpInstance();
-
-    if (ncp != nullptr)
-    {
-        ncp->RegisterLegacyHandlers(aHandlers);
-    }
-
-#else
-    OT_UNUSED_VARIABLE(aHandlers);
-#endif
-}
-
-void otNcpHandleDidReceiveNewLegacyUlaPrefix(const uint8_t *aUlaPrefix)
-{
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-    ot::Ncp::NcpBase *ncp = ot::Ncp::NcpBase::GetNcpInstance();
-
-    if (ncp != nullptr)
-    {
-        ncp->HandleDidReceiveNewLegacyUlaPrefix(aUlaPrefix);
-    }
-
-#else
-    OT_UNUSED_VARIABLE(aUlaPrefix);
-#endif
-}
-
-void otNcpHandleLegacyNodeDidJoin(const otExtAddress *aExtAddr)
-{
-#if OPENTHREAD_CONFIG_LEGACY_ENABLE
-    ot::Ncp::NcpBase *ncp = ot::Ncp::NcpBase::GetNcpInstance();
-
-    if (ncp != nullptr)
-    {
-        ncp->HandleLegacyNodeDidJoin(aExtAddr);
-    }
-
-#else
-    OT_UNUSED_VARIABLE(aExtAddr);
-#endif
-}
-
 #endif // OPENTHREAD_MTD || OPENTHREAD_FTD
diff --git a/src/ncp/ncp_base_radio.cpp b/src/ncp/ncp_base_radio.cpp
index 975cc3f..11ec6aa 100644
--- a/src/ncp/ncp_base_radio.cpp
+++ b/src/ncp/ncp_base_radio.cpp
@@ -53,6 +53,11 @@
 {
     return mEncoder.WriteUintPacked(SPINEL_RCP_API_VERSION);
 }
+
+template <> otError NcpBase::HandlePropertyGet<SPINEL_PROP_RCP_MIN_HOST_API_VERSION>(void)
+{
+    return mEncoder.WriteUintPacked(SPINEL_MIN_HOST_SUPPORTED_RCP_API_VERSION);
+}
 #endif
 
 // ----------------------------------------------------------------------------
@@ -403,14 +408,15 @@
     SuccessOrExit(error = mDecoder.ReadUint8(aFrame.mChannel));
 
     // Set the default value for all optional parameters.
-    aFrame.mInfo.mTxInfo.mMaxCsmaBackoffs     = OPENTHREAD_CONFIG_MAC_MAX_CSMA_BACKOFFS_DIRECT;
-    aFrame.mInfo.mTxInfo.mMaxFrameRetries     = OPENTHREAD_CONFIG_MAC_DEFAULT_MAX_FRAME_RETRIES_DIRECT;
-    aFrame.mInfo.mTxInfo.mCsmaCaEnabled       = true;
-    aFrame.mInfo.mTxInfo.mIsHeaderUpdated     = false;
-    aFrame.mInfo.mTxInfo.mIsARetx             = false;
-    aFrame.mInfo.mTxInfo.mIsSecurityProcessed = false;
-    aFrame.mInfo.mTxInfo.mTxDelay             = 0;
-    aFrame.mInfo.mTxInfo.mTxDelayBaseTime     = 0;
+    aFrame.mInfo.mTxInfo.mRxChannelAfterTxDone = aFrame.mChannel;
+    aFrame.mInfo.mTxInfo.mMaxCsmaBackoffs      = OPENTHREAD_CONFIG_MAC_MAX_CSMA_BACKOFFS_DIRECT;
+    aFrame.mInfo.mTxInfo.mMaxFrameRetries      = OPENTHREAD_CONFIG_MAC_DEFAULT_MAX_FRAME_RETRIES_DIRECT;
+    aFrame.mInfo.mTxInfo.mCsmaCaEnabled        = true;
+    aFrame.mInfo.mTxInfo.mIsHeaderUpdated      = false;
+    aFrame.mInfo.mTxInfo.mIsARetx              = false;
+    aFrame.mInfo.mTxInfo.mIsSecurityProcessed  = false;
+    aFrame.mInfo.mTxInfo.mTxDelay              = 0;
+    aFrame.mInfo.mTxInfo.mTxDelayBaseTime      = 0;
 
     // All the next parameters are optional. Note that even if the
     // decoder fails to parse any of optional parameters we still want to
@@ -419,16 +425,22 @@
 
     SuccessOrExit(mDecoder.ReadUint8(aFrame.mInfo.mTxInfo.mMaxCsmaBackoffs));
     SuccessOrExit(mDecoder.ReadUint8(aFrame.mInfo.mTxInfo.mMaxFrameRetries));
+
     SuccessOrExit(mDecoder.ReadBool(csmaEnable));
+    aFrame.mInfo.mTxInfo.mCsmaCaEnabled = csmaEnable;
+
     SuccessOrExit(mDecoder.ReadBool(isHeaderUpdated));
+    aFrame.mInfo.mTxInfo.mIsHeaderUpdated = isHeaderUpdated;
+
     SuccessOrExit(mDecoder.ReadBool(isARetx));
+    aFrame.mInfo.mTxInfo.mIsARetx = isARetx;
+
     SuccessOrExit(mDecoder.ReadBool(isSecurityProcessed));
+    aFrame.mInfo.mTxInfo.mIsSecurityProcessed = isSecurityProcessed;
+
     SuccessOrExit(mDecoder.ReadUint32(aFrame.mInfo.mTxInfo.mTxDelay));
     SuccessOrExit(mDecoder.ReadUint32(aFrame.mInfo.mTxInfo.mTxDelayBaseTime));
-    aFrame.mInfo.mTxInfo.mCsmaCaEnabled       = csmaEnable;
-    aFrame.mInfo.mTxInfo.mIsHeaderUpdated     = isHeaderUpdated;
-    aFrame.mInfo.mTxInfo.mIsARetx             = isARetx;
-    aFrame.mInfo.mTxInfo.mIsSecurityProcessed = isSecurityProcessed;
+    SuccessOrExit(mDecoder.ReadUint8(aFrame.mInfo.mTxInfo.mRxChannelAfterTxDone));
 
 exit:
     return error;
@@ -446,12 +458,12 @@
 
     SuccessOrExit(error = DecodeStreamRawTxRequest(*frame));
 
-    // Cache the transaction ID for async response
-    mCurTransmitTID = SPINEL_HEADER_GET_TID(aHeader);
-
     // Pass frame to the radio layer. Note, this fails if we
     // haven't enabled raw stream or are already transmitting.
-    error = otLinkRawTransmit(mInstance, &NcpBase::LinkRawTransmitDone);
+    SuccessOrExit(error = otLinkRawTransmit(mInstance, &NcpBase::LinkRawTransmitDone));
+
+    // Cache the transaction ID for async response
+    mCurTransmitTID = SPINEL_HEADER_GET_TID(aHeader);
 
 exit:
 
@@ -503,10 +515,23 @@
 {
     otError  error = OT_ERROR_NONE;
     uint32_t frameCounter;
+    bool     setIfLarger = false;
 
     SuccessOrExit(error = mDecoder.ReadUint32(frameCounter));
 
-    error = otLinkRawSetMacFrameCounter(mInstance, frameCounter);
+    if (!mDecoder.IsAllReadInStruct())
+    {
+        SuccessOrExit(error = mDecoder.ReadBool(setIfLarger));
+    }
+
+    if (setIfLarger)
+    {
+        error = otLinkRawSetMacFrameCounterIfLarger(mInstance, frameCounter);
+    }
+    else
+    {
+        error = otLinkRawSetMacFrameCounter(mInstance, frameCounter);
+    }
 
 exit:
     return error;
diff --git a/src/ncp/ncp_hdlc.cpp b/src/ncp/ncp_hdlc.cpp
index b1a56c3..2020d15 100644
--- a/src/ncp/ncp_hdlc.cpp
+++ b/src/ncp/ncp_hdlc.cpp
@@ -66,7 +66,7 @@
 
 extern "C" void otNcpHdlcInit(otInstance *aInstance, otNcpHdlcSendCallback aSendCallback)
 {
-    NcpHdlc * ncpHdlc  = nullptr;
+    NcpHdlc  *ncpHdlc  = nullptr;
     Instance *instance = static_cast<Instance *>(aInstance);
 
     ncpHdlc = new (&sNcpRaw) NcpHdlc(instance, aSendCallback);
@@ -95,10 +95,10 @@
     mTxFrameBuffer.SetFrameAddedCallback(HandleFrameAddedToNcpBuffer, this);
 }
 
-void NcpHdlc::HandleFrameAddedToNcpBuffer(void *                   aContext,
+void NcpHdlc::HandleFrameAddedToNcpBuffer(void                    *aContext,
                                           Spinel::Buffer::FrameTag aTag,
                                           Spinel::Buffer::Priority aPriority,
-                                          Spinel::Buffer *         aBuffer)
+                                          Spinel::Buffer          *aBuffer)
 {
     OT_UNUSED_VARIABLE(aBuffer);
     OT_UNUSED_VARIABLE(aTag);
@@ -240,10 +240,7 @@
     mFrameDecoder.Decode(aBuf, aBufLength);
 }
 
-void NcpHdlc::HandleFrame(void *aContext, otError aError)
-{
-    static_cast<NcpHdlc *>(aContext)->HandleFrame(aError);
-}
+void NcpHdlc::HandleFrame(void *aContext, otError aError) { static_cast<NcpHdlc *>(aContext)->HandleFrame(aError); }
 
 void NcpHdlc::HandleFrame(otError aError)
 {
@@ -277,8 +274,6 @@
 
     super_t::IncrementFrameErrorCounter();
 
-    // We can get away with sprintf because we know
-    // `hexbuf` is large enough.
     snprintf(hexbuf, sizeof(hexbuf), "Framing error %d: [", aError);
 
     // Write out the first part of our log message.
@@ -288,9 +283,6 @@
     // The second '3' comes from the length of two hex digits and a space.
     for (i = 0; (i < aBufLength) && (i < (sizeof(hexbuf) - 3) / 3); i++)
     {
-        // We can get away with sprintf because we know
-        // `hexbuf` is large enough, based on our calculations
-        // above.
         snprintf(&hexbuf[i * 3], sizeof(hexbuf) - i * 3, " %02X", static_cast<uint8_t>(aBuf[i]));
     }
 
@@ -312,10 +304,7 @@
 {
 }
 
-bool NcpHdlc::BufferEncrypterReader::IsEmpty(void) const
-{
-    return mTxFrameBuffer.IsEmpty() && !mOutputDataLength;
-}
+bool NcpHdlc::BufferEncrypterReader::IsEmpty(void) const { return mTxFrameBuffer.IsEmpty() && !mOutputDataLength; }
 
 otError NcpHdlc::BufferEncrypterReader::OutFrameBegin(void)
 {
@@ -347,20 +336,11 @@
     return status;
 }
 
-bool NcpHdlc::BufferEncrypterReader::OutFrameHasEnded(void)
-{
-    return (mDataBufferReadIndex >= mOutputDataLength);
-}
+bool NcpHdlc::BufferEncrypterReader::OutFrameHasEnded(void) { return (mDataBufferReadIndex >= mOutputDataLength); }
 
-uint8_t NcpHdlc::BufferEncrypterReader::OutFrameReadByte(void)
-{
-    return mDataBuffer[mDataBufferReadIndex++];
-}
+uint8_t NcpHdlc::BufferEncrypterReader::OutFrameReadByte(void) { return mDataBuffer[mDataBufferReadIndex++]; }
 
-otError NcpHdlc::BufferEncrypterReader::OutFrameRemove(void)
-{
-    return mTxFrameBuffer.OutFrameRemove();
-}
+otError NcpHdlc::BufferEncrypterReader::OutFrameRemove(void) { return mTxFrameBuffer.OutFrameRemove(); }
 
 void NcpHdlc::BufferEncrypterReader::Reset(void)
 {
diff --git a/src/ncp/ncp_hdlc.hpp b/src/ncp/ncp_hdlc.hpp
index e3c489f..d0516c0 100644
--- a/src/ncp/ncp_hdlc.hpp
+++ b/src/ncp/ncp_hdlc.hpp
@@ -122,15 +122,15 @@
 
     static void           EncodeAndSend(Tasklet &aTasklet);
     static void           HandleFrame(void *aContext, otError aError);
-    static void           HandleFrameAddedToNcpBuffer(void *                   aContext,
+    static void           HandleFrameAddedToNcpBuffer(void                    *aContext,
                                                       Spinel::Buffer::FrameTag aTag,
                                                       Spinel::Buffer::Priority aPriority,
-                                                      Spinel::Buffer *         aBuffer);
+                                                      Spinel::Buffer          *aBuffer);
     otNcpHdlcSendCallback mSendCallback;
 
+    Hdlc::FrameBuffer<kHdlcTxBufferSize> mHdlcBuffer;
     Hdlc::Encoder                        mFrameEncoder;
     Hdlc::Decoder                        mFrameDecoder;
-    Hdlc::FrameBuffer<kHdlcTxBufferSize> mHdlcBuffer;
     HdlcTxState                          mState;
     uint8_t                              mByte;
     Hdlc::FrameBuffer<kRxBufferSize>     mRxBuffer;
diff --git a/src/ncp/ncp_spi.cpp b/src/ncp/ncp_spi.cpp
index 618769b..9b2a472 100644
--- a/src/ncp/ncp_spi.cpp
+++ b/src/ncp/ncp_spi.cpp
@@ -64,7 +64,7 @@
 
 extern "C" void otNcpSpiInit(otInstance *aInstance)
 {
-    NcpSpi *  ncpSpi   = nullptr;
+    NcpSpi   *ncpSpi   = nullptr;
     Instance *instance = static_cast<Instance *>(aInstance);
 
     ncpSpi = new (&sNcpRaw) NcpSpi(instance);
@@ -114,7 +114,7 @@
                                                  /* aRequestTransactionFlag */ true));
 }
 
-bool NcpSpi::SpiTransactionComplete(void *   aContext,
+bool NcpSpi::SpiTransactionComplete(void    *aContext,
                                     uint8_t *aOutputBuf,
                                     uint16_t aOutputLen,
                                     uint8_t *aInputBuf,
@@ -224,10 +224,7 @@
     return shouldProcess;
 }
 
-void NcpSpi::SpiTransactionProcess(void *aContext)
-{
-    reinterpret_cast<NcpSpi *>(aContext)->SpiTransactionProcess();
-}
+void NcpSpi::SpiTransactionProcess(void *aContext) { reinterpret_cast<NcpSpi *>(aContext)->SpiTransactionProcess(); }
 
 void NcpSpi::SpiTransactionProcess(void)
 {
@@ -242,10 +239,10 @@
     }
 }
 
-void NcpSpi::HandleFrameAddedToTxBuffer(void *                   aContext,
+void NcpSpi::HandleFrameAddedToTxBuffer(void                    *aContext,
                                         Spinel::Buffer::FrameTag aTag,
                                         Spinel::Buffer::Priority aPriority,
-                                        Spinel::Buffer *         aBuffer)
+                                        Spinel::Buffer          *aBuffer)
 {
     OT_UNUSED_VARIABLE(aBuffer);
     OT_UNUSED_VARIABLE(aTag);
diff --git a/src/ncp/ncp_spi.hpp b/src/ncp/ncp_spi.hpp
index d4a87dd..43c80db 100644
--- a/src/ncp/ncp_spi.hpp
+++ b/src/ncp/ncp_spi.hpp
@@ -278,7 +278,7 @@
     typedef uint8_t LargeFrameBuffer[kSpiBufferSize];
     typedef uint8_t EmptyFrameBuffer[kSpiHeaderSize];
 
-    static bool SpiTransactionComplete(void *   aContext,
+    static bool SpiTransactionComplete(void    *aContext,
                                        uint8_t *aOutputBuf,
                                        uint16_t aOutputLen,
                                        uint8_t *aInputBuf,
@@ -293,10 +293,10 @@
     static void SpiTransactionProcess(void *aContext);
     void        SpiTransactionProcess(void);
 
-    static void HandleFrameAddedToTxBuffer(void *                   aContext,
+    static void HandleFrameAddedToTxBuffer(void                    *aContext,
                                            Spinel::Buffer::FrameTag aFrameTag,
                                            Spinel::Buffer::Priority aPriority,
-                                           Spinel::Buffer *         aBuffer);
+                                           Spinel::Buffer          *aBuffer);
 
     static void PrepareTxFrame(Tasklet &aTasklet);
     void        PrepareTxFrame(void);
diff --git a/src/ncp/radio.cmake b/src/ncp/radio.cmake
index 427e7b5..1350a28 100644
--- a/src/ncp/radio.cmake
+++ b/src/ncp/radio.cmake
@@ -31,9 +31,14 @@
 target_compile_definitions(openthread-rcp PRIVATE
     OPENTHREAD_RADIO=1
     OPENTHREAD_RADIO_CLI=0
-    OPENTHREAD_CONFIG_NCP_HDLC_ENABLE=1
 )
 
+if (OT_NCP_SPI)
+    target_compile_definitions(openthread-rcp PRIVATE OPENTHREAD_CONFIG_NCP_HDLC_ENABLE=0)
+else()
+    target_compile_definitions(openthread-rcp PRIVATE OPENTHREAD_CONFIG_NCP_HDLC_ENABLE=1)
+endif()
+
 target_compile_options(openthread-rcp PRIVATE
     ${OT_CFLAGS}
 )
@@ -47,7 +52,11 @@
     PUBLIC
         openthread-radio
     PRIVATE
-        openthread-hdlc
         openthread-spinel-rcp
+        ot-config-radio
         ot-config
 )
+
+if(NOT OT_NCP_SPI)
+    target_link_libraries(openthread-rcp PRIVATE openthread-hdlc)
+endif()
diff --git a/src/posix/Makefile-posix b/src/posix/Makefile-posix
index 24c7a83..7fe9722 100644
--- a/src/posix/Makefile-posix
+++ b/src/posix/Makefile-posix
@@ -44,7 +44,6 @@
 COMMISSIONER                         ?= 1
 CHANNEL_MANAGER                      ?= 1
 CHANNEL_MONITOR                      ?= 1
-CHILD_SUPERVISION                    ?= 1
 DAEMON                               ?= 0
 DATASET_UPDATER                      ?= 1
 DHCP6_CLIENT                         ?= 1
@@ -58,14 +57,13 @@
 IP6_FRAGM                            ?= 1
 JAM_DETECTION                        ?= 1
 JOINER                               ?= 1
-LEGACY                               ?= 1
 LINK_RAW                             ?= 0
 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
@@ -145,6 +143,7 @@
 
 LDFLAGS                        += \
     $(COMMONCFLAGS)               \
+    -rdynamic                     \
     $(NULL)
 
 TopSourceDir                   := $(dir $(shell readlink $(firstword $(MAKEFILE_LIST))))../..
diff --git a/src/posix/Makefile.am b/src/posix/Makefile.am
index 3844ea1..c0a0766 100644
--- a/src/posix/Makefile.am
+++ b/src/posix/Makefile.am
@@ -62,6 +62,7 @@
 
 if OPENTHREAD_TARGET_LINUX
 LDADD_COMMON                                                          += \
+    -lanl                                                                \
     -lrt                                                                 \
     $(NULL)
 endif
diff --git a/src/posix/README.md b/src/posix/README.md
index ced9af5..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
 
@@ -77,19 +77,25 @@
 
    ```sh
    rm -rf build
-   script/build nrf52840 USB_trans -DOT_BOOTLOADER=USB -DOT_THREAD_VERSION=1.2
+   script/build nrf52840 USB_trans -DOT_BOOTLOADER=USB
    ```
 
    b. For nRF52840 Development Kit
 
    ```sh
    rm -rf build
-   script/build nrf52840 UART_trans -DOT_THREAD_VERSION=1.2
+   script/build nrf52840 UART_trans
    ```
 
    This creates an RCP image at `build/bin/ot-rcp`.
 
-5. Depending on the hardware platform, complete the following steps to program the device:
+5. Generate the HEX image:
+
+   ```sh
+   arm-none-eabi-objcopy -O ihex build/bin/ot-rcp build/bin/ot-rcp.hex
+   ```
+
+6. Depending on the hardware platform, complete the following steps to program the device:
 
    a. nRF52840 Dongle (USB transport)
 
@@ -133,28 +139,6 @@
 ./build/posix/src/posix/ot-cli 'spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=115200'
 ```
 
-### CC2538
-
-#### Build
-
-```
-./script/cmake-build cc2538 -DOT_APP_CLI=OFF -DOT_APP_NCP=OFF -DOT_FTD=OFF -DOT_MTD=OFF
-```
-
-#### Flash
-
-```sh
-arm-none-eabi-objcopy -O ihex build/cc2538/examples/apps/ncp/ot-rcp ot-rcp.bin
-# see https://github.com/JelmerT/cc2538-bsl
-python cc2538-bsl/cc2538-bsl.py -b 460800 -e -w -v -p /dev/ttyUSB0 ot-rcp.bin
-```
-
-#### Run
-
-```sh
-./build/posix/src/posix/ot-cli 'spinel+hdlc+uart:///dev/ttyUSB0?uart-baudrate=115200'
-```
-
 ## Daemon Mode
 
 OpenThread Posix Daemon mode uses a unix socket as input and output, so that OpenThread core can run as a service. And a client can communicate with it by connecting to the socket. The protocol is OpenThread CLI.
diff --git a/src/posix/cli.cmake b/src/posix/cli.cmake
index e9b2e10..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
@@ -53,9 +53,17 @@
     openthread-spinel-rcp
     ${OT_MBEDTLS}
     ${READLINE_LINK_LIBRARIES}
+    ot-config-ftd
     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/cli_readline.cpp b/src/posix/cli_readline.cpp
index 7fd1b0b..3a402cb 100644
--- a/src/posix/cli_readline.cpp
+++ b/src/posix/cli_readline.cpp
@@ -94,10 +94,7 @@
     otCliInit(aInstance, OutputCallback, nullptr);
 }
 
-extern "C" void otAppCliDeinit(void)
-{
-    rl_callback_handler_remove();
-}
+extern "C" void otAppCliDeinit(void) { rl_callback_handler_remove(); }
 
 extern "C" void otAppCliUpdate(otSysMainloopContext *aMainloop)
 {
diff --git a/src/posix/cli_stdio.cpp b/src/posix/cli_stdio.cpp
index e6c62d7..54a194c 100644
--- a/src/posix/cli_stdio.cpp
+++ b/src/posix/cli_stdio.cpp
@@ -58,14 +58,9 @@
 }
 } // namespace
 
-extern "C" void otAppCliInit(otInstance *aInstance)
-{
-    otCliInit(aInstance, OutputCallback, nullptr);
-}
+extern "C" void otAppCliInit(otInstance *aInstance) { otCliInit(aInstance, OutputCallback, nullptr); }
 
-extern "C" void otAppCliDeinit(void)
-{
-}
+extern "C" void otAppCliDeinit(void) {}
 
 extern "C" void otAppCliUpdate(otSysMainloopContext *aMainloop)
 {
diff --git a/src/posix/client.cpp b/src/posix/client.cpp
index e9128b6..632e9c9 100644
--- a/src/posix/client.cpp
+++ b/src/posix/client.cpp
@@ -238,7 +238,7 @@
 
 Config ParseArg(int &aArgCount, char **&aArgVector)
 {
-    Config config = {"wpan0"};
+    Config config = {OPENTHREAD_POSIX_CONFIG_THREAD_NETIF_DEFAULT_NAME};
 
     optind = 1;
 
@@ -391,7 +391,7 @@
                     lineBuffer[lineBufferWritePos++] = c;
                     if (c == '\n' || lineBufferWritePos >= sizeof(lineBuffer) - 1)
                     {
-                        char * line = lineBuffer;
+                        char  *line = lineBuffer;
                         size_t len  = lineBufferWritePos;
 
                         // read one line successfully or line buffer is full
diff --git a/src/posix/daemon.cmake b/src/posix/daemon.cmake
index 7116a4a..dd726df 100644
--- a/src/posix/daemon.cmake
+++ b/src/posix/daemon.cmake
@@ -45,9 +45,18 @@
     openthread-spinel-rcp
     ${OT_MBEDTLS}
     ot-posix-config
+    ot-config-ftd
     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
 )
@@ -67,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 df014ea..a7eb077 100644
--- a/src/posix/main.c
+++ b/src/posix/main.c
@@ -141,6 +141,7 @@
     OT_POSIX_OPT_DRY_RUN                 = 'n',
     OT_POSIX_OPT_HELP                    = 'h',
     OT_POSIX_OPT_INTERFACE_NAME          = 'I',
+    OT_POSIX_OPT_PERSISTENT_INTERFACE    = 'p',
     OT_POSIX_OPT_TIME_SPEED              = 's',
     OT_POSIX_OPT_VERBOSE                 = 'v',
 
@@ -156,6 +157,7 @@
     {"dry-run", no_argument, NULL, OT_POSIX_OPT_DRY_RUN},
     {"help", no_argument, NULL, OT_POSIX_OPT_HELP},
     {"interface-name", required_argument, NULL, OT_POSIX_OPT_INTERFACE_NAME},
+    {"persistent-interface", no_argument, NULL, OT_POSIX_OPT_PERSISTENT_INTERFACE},
     {"radio-version", no_argument, NULL, OT_POSIX_OPT_RADIO_VERSION},
     {"real-time-signal", required_argument, NULL, OT_POSIX_OPT_REAL_TIME_SIGNAL},
     {"time-speed", required_argument, NULL, OT_POSIX_OPT_TIME_SPEED},
@@ -174,6 +176,7 @@
             "    -I  --interface-name name     Thread network interface name.\n"
             "    -n  --dry-run                 Just verify if arguments is valid and radio spinel is compatible.\n"
             "        --radio-version           Print radio firmware version.\n"
+            "    -p  --persistent-interface    Persistent the created thread network interface\n"
             "    -s  --time-speed factor       Time speed up factor.\n"
             "    -v  --verbose                 Also log to stderr.\n",
             aProgramName);
@@ -191,8 +194,10 @@
 {
     memset(aConfig, 0, sizeof(*aConfig));
 
-    aConfig->mPlatformConfig.mSpeedUpFactor = 1;
-    aConfig->mLogLevel                      = OT_LOG_LEVEL_CRIT;
+    aConfig->mPlatformConfig.mPersistentInterface = false;
+    aConfig->mPlatformConfig.mSpeedUpFactor       = 1;
+    aConfig->mLogLevel                            = OT_LOG_LEVEL_CRIT;
+    aConfig->mPlatformConfig.mInterfaceName       = OPENTHREAD_POSIX_CONFIG_THREAD_NETIF_DEFAULT_NAME;
 #ifdef __linux__
     aConfig->mPlatformConfig.mRealTimeSignal = SIGRTMIN;
 #endif
@@ -202,7 +207,7 @@
     while (true)
     {
         int index  = 0;
-        int option = getopt_long(aArgCount, aArgVector, "B:d:hI:ns:v", kOptions, &index);
+        int option = getopt_long(aArgCount, aArgVector, "B:d:hI:nps:v", kOptions, &index);
 
         if (option == -1)
         {
@@ -220,6 +225,9 @@
         case OT_POSIX_OPT_INTERFACE_NAME:
             aConfig->mPlatformConfig.mInterfaceName = optarg;
             break;
+        case OT_POSIX_OPT_PERSISTENT_INTERFACE:
+            aConfig->mPlatformConfig.mPersistentInterface = true;
+            break;
         case OT_POSIX_OPT_BACKBONE_INTERFACE_NAME:
             aConfig->mPlatformConfig.mBackboneInterfaceName = optarg;
             break;
@@ -308,10 +316,7 @@
     return instance;
 }
 
-void otTaskletsSignalPending(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
+void otTaskletsSignalPending(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
 
 void otPlatReset(otInstance *aInstance)
 {
@@ -325,17 +330,19 @@
     assert(false);
 }
 
-static void ProcessNetif(void *aContext, uint8_t aArgsLength, char *aArgs[])
+static otError ProcessNetif(void *aContext, uint8_t aArgsLength, char *aArgs[])
 {
     OT_UNUSED_VARIABLE(aContext);
     OT_UNUSED_VARIABLE(aArgsLength);
     OT_UNUSED_VARIABLE(aArgs);
 
     otCliOutputFormat("%s:%u\r\n", otSysGetThreadNetifName(), otSysGetThreadNetifIndex());
+
+    return OT_ERROR_NONE;
 }
 
 #if !OPENTHREAD_POSIX_CONFIG_DAEMON_ENABLE
-static void ProcessExit(void *aContext, uint8_t aArgsLength, char *aArgs[])
+static otError ProcessExit(void *aContext, uint8_t aArgsLength, char *aArgs[])
 {
     OT_UNUSED_VARIABLE(aContext);
     OT_UNUSED_VARIABLE(aArgsLength);
@@ -374,7 +381,10 @@
 #if !OPENTHREAD_POSIX_CONFIG_DAEMON_ENABLE
     otAppCliInit(instance);
 #endif
-    otCliSetUserCommands(kCommands, OT_ARRAY_LENGTH(kCommands), instance);
+
+#if !OPENTHREAD_POSIX_CONFIG_DAEMON_ENABLE || OPENTHREAD_POSIX_CONFIG_DAEMON_CLI_ENABLE
+    IgnoreError(otCliSetUserCommands(kCommands, OT_ARRAY_LENGTH(kCommands), instance));
+#endif
 
     while (true)
     {
diff --git a/src/posix/platform/CMakeLists.txt b/src/posix/platform/CMakeLists.txt
index 701eb21..02a30dc 100644
--- a/src/posix/platform/CMakeLists.txt
+++ b/src/posix/platform/CMakeLists.txt
@@ -35,6 +35,14 @@
     target_compile_definitions(ot-posix-config
         INTERFACE "OPENTHREAD_POSIX_CONFIG_DAEMON_ENABLE=1"
     )
+
+    # We have to add this definition to `ot-config` because openthread core
+    # libraries will set config file to "openthrad-core-posix-config.h" which
+    # depends on `OPENTHREAD_POSIX_CONFIG_DAEMON_ENABLE` to correctly enable
+    # PLATFORM_NETIF and PLATFORM_UDP features. Otherwise, openthread core and
+    # posix libraries will use different feature definitions.
+    list(APPEND OT_PLATFORM_DEFINES "OPENTHREAD_POSIX_CONFIG_DAEMON_ENABLE=1")
+    set(OT_PLATFORM_DEFINES ${OT_PLATFORM_DEFINES} PARENT_SCOPE)
 endif()
 
 option(OT_POSIX_INSTALL_EXTERNAL_ROUTES "Install External Routes as IPv6 routes" ON)
@@ -76,16 +84,28 @@
     )
 endif()
 
+set(OT_POSIX_NAT64_CIDR "192.168.255.0/24" CACHE STRING "NAT64 CIDR for OpenThread NAT64")
+if(OT_POSIX_NAT64_CIDR)
+    target_compile_definitions(ot-posix-config
+        INTERFACE "OPENTHREAD_POSIX_CONFIG_NAT64_CIDR=\"${OT_POSIX_NAT64_CIDR}\""
+    )
+endif()
+
 if(NOT OT_CONFIG)
     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
+    backtrace.cpp
+    config_file.cpp
     daemon.cpp
     entropy.cpp
     firewall.cpp
@@ -97,8 +117,11 @@
     misc.cpp
     multicast_routing.cpp
     netif.cpp
+    power.cpp
+    power_updater.cpp
     radio.cpp
     radio_url.cpp
+    resolver.cpp
     settings.cpp
     spi_interface.cpp
     system.cpp
@@ -106,20 +129,34 @@
     udp.cpp
     utils.cpp
     virtual_time.cpp
-    ${OT_POSIX_CONFIG_RCP_VENDOR_INTERFACE}
 )
 
+include(vendor.cmake)
+
 target_link_libraries(openthread-posix
     PUBLIC
         openthread-platform
     PRIVATE
+        openthread-cli-ftd
+        openthread-ftd
+        openthread-hdlc
+        openthread-spinel-rcp
         openthread-url
+        ot-config-ftd
         ot-config
         ot-posix-config
-        util
+        $<$<NOT:$<BOOL:${OT_ANDROID_NDK}>>:util>
         $<$<STREQUAL:${CMAKE_SYSTEM_NAME},Linux>:rt>
 )
 
+option(OT_TARGET_OPENWRT "enable openthread posix for OpenWRT" OFF)
+if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux" AND NOT OT_TARGET_OPENWRT)
+    target_compile_definitions(ot-posix-config
+        INTERFACE "OPENTHREAD_POSIX_CONFIG_NAT64_AIL_PREFIX_ENABLE=1"
+    )
+    target_link_libraries(openthread-posix PRIVATE anl)
+endif()
+
 target_compile_definitions(openthread-posix
     PUBLIC
         ${OT_PUBLIC_DEFINES}
@@ -140,3 +177,18 @@
     PUBLIC
         ${PROJECT_SOURCE_DIR}/src/posix/platform/include
 )
+
+add_executable(ot-posix-test-settings
+    settings.cpp
+)
+target_compile_definitions(ot-posix-test-settings
+    PRIVATE -DSELF_TEST=1 -DOPENTHREAD_CONFIG_LOG_PLATFORM=0
+)
+target_include_directories(ot-posix-test-settings
+    PRIVATE
+        ${PROJECT_SOURCE_DIR}/include
+        ${PROJECT_SOURCE_DIR}/src
+        ${PROJECT_SOURCE_DIR}/src/core
+        ${PROJECT_SOURCE_DIR}/src/posix/platform/include
+)
+add_test(NAME ot-posix-test-settings COMMAND ot-posix-test-settings)
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/Makefile.am b/src/posix/platform/Makefile.am
index ab7ae85..ae51023 100644
--- a/src/posix/platform/Makefile.am
+++ b/src/posix/platform/Makefile.am
@@ -46,6 +46,8 @@
 libopenthread_posix_a_SOURCES             = \
     alarm.cpp                               \
     backbone.cpp                            \
+    backtrace.cpp                           \
+    config_file.cpp                         \
     daemon.cpp                              \
     entropy.cpp                             \
     firewall.cpp                            \
@@ -57,7 +59,10 @@
     misc.cpp                                \
     multicast_routing.cpp                   \
     netif.cpp                               \
+    power.cpp                               \
+    power_updater.cpp                       \
     radio.cpp                               \
+    resolver.cpp                            \
     radio_url.cpp                           \
     settings.cpp                            \
     spi_interface.cpp                       \
@@ -93,23 +98,4 @@
 CLEANFILES                                = $(wildcard *.gcda *.gcno)
 endif # OPENTHREAD_BUILD_COVERAGE
 
-check_PROGRAMS = test-settings
-
-test_settings_CPPFLAGS                                        = \
-    -I$(top_srcdir)/include                                     \
-    -I$(top_srcdir)/src                                         \
-    -I$(top_srcdir)/src/core                                    \
-    -I$(top_srcdir)/src/posix/platform/include                  \
-    -DOPENTHREAD_CONFIG_LOG_PLATFORM=0                          \
-    -DSELF_TEST                                                 \
-    $(NULL)
-
-test_settings_SOURCES                     = \
-    settings.cpp                            \
-    $(NULL)
-
-TESTS                                     = \
-    test-settings                           \
-    $(NULL)
-
 include $(abs_top_nlbuild_autotools_dir)/automake/post.am
diff --git a/src/posix/platform/alarm.cpp b/src/posix/platform/alarm.cpp
index 776734f..a739a56 100644
--- a/src/posix/platform/alarm.cpp
+++ b/src/posix/platform/alarm.cpp
@@ -87,10 +87,7 @@
 }
 #endif // !OPENTHREAD_POSIX_VIRTUAL_TIME
 
-static uint64_t platformAlarmGetNow(void)
-{
-    return otPlatTimeGet() * sSpeedUpFactor;
-}
+static uint64_t platformAlarmGetNow(void) { return otPlatTimeGet() * sSpeedUpFactor; }
 
 void platformAlarmInit(uint32_t aSpeedUpFactor, int aRealTimeSignal)
 {
@@ -131,10 +128,7 @@
     }
 }
 
-uint32_t otPlatAlarmMilliGetNow(void)
-{
-    return (uint32_t)(platformAlarmGetNow() / US_PER_MS);
-}
+uint32_t otPlatAlarmMilliGetNow(void) { return (uint32_t)(platformAlarmGetNow() / US_PER_MS); }
 
 void otPlatAlarmMilliStartAt(otInstance *aInstance, uint32_t aT0, uint32_t aDt)
 {
@@ -152,10 +146,7 @@
 }
 
 #if OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
-uint32_t otPlatAlarmMicroGetNow(void)
-{
-    return static_cast<uint32_t>(platformAlarmGetNow());
-}
+uint32_t otPlatAlarmMicroGetNow(void) { return static_cast<uint32_t>(platformAlarmGetNow()); }
 
 void otPlatAlarmMicroStartAt(otInstance *aInstance, uint32_t aT0, uint32_t aDt)
 {
diff --git a/src/posix/platform/backtrace.cpp b/src/posix/platform/backtrace.cpp
new file mode 100644
index 0000000..1f697ca
--- /dev/null
+++ b/src/posix/platform/backtrace.cpp
@@ -0,0 +1,178 @@
+/*
+ *  Copyright (c) 2022, 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 backtrace for posix.
+ */
+
+#include "openthread-posix-config.h"
+#include "platform-posix.h"
+
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "common/code_utils.hpp"
+#include "common/logging.hpp"
+
+#if OPENTHREAD_POSIX_CONFIG_BACKTRACE_ENABLE
+#if OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE || defined(__GLIBC__)
+#if OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE
+#include <log/log.h>
+#include <utils/CallStack.h>
+#include <utils/Printer.h>
+
+class LogPrinter : public android::Printer
+{
+public:
+    virtual void printLine(const char *string) { otLogCritPlat("%s", string); }
+};
+
+static void dumpStack(void)
+{
+    LogPrinter         printer;
+    android::CallStack callstack;
+
+    callstack.update();
+    callstack.print(printer);
+}
+#else // OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE
+#include <cxxabi.h>
+#include <execinfo.h>
+
+static void demangleSymbol(int aIndex, const char *aSymbol)
+{
+    constexpr uint16_t kMaxNameSize = 256;
+    int                status;
+    char               program[kMaxNameSize + 1];
+    char               mangledName[kMaxNameSize + 1];
+    char              *demangledName = nullptr;
+    char              *functionName  = nullptr;
+    unsigned int       offset        = 0;
+    unsigned int       address       = 0;
+
+    // Try to demangle a C++ name
+    if (sscanf(aSymbol, "%256[^(]%*[^_]%256[^)+]%*[^0x]%x%*[^0x]%x", program, mangledName, &offset, &address) == 4)
+    {
+        demangledName = abi::__cxa_demangle(mangledName, nullptr, nullptr, &status);
+        functionName  = demangledName;
+    }
+
+    VerifyOrExit(demangledName == nullptr);
+
+    // If failed to demangle a C++ name, try to get a regular C symbol
+    if (sscanf(aSymbol, "%256[^(](%256[^)+]%*[^0x]%x%*[^0x]%x", program, mangledName, &offset, &address) == 4)
+    {
+        functionName = mangledName;
+    }
+
+exit:
+    if (functionName != nullptr)
+    {
+        otLogCritPlat("#%2d: %s %s+0x%x [0x%x]\n", aIndex, program, functionName, offset, address);
+    }
+    else
+    {
+        otLogCritPlat("#%2d: %s\n", aIndex, aSymbol);
+    }
+
+    if (demangledName != nullptr)
+    {
+        free(demangledName);
+    }
+}
+
+static void dumpStack(void)
+{
+    constexpr uint8_t kMaxDepth = 50;
+    void             *stack[kMaxDepth];
+    char            **symbols;
+    int               depth;
+
+    depth   = backtrace(stack, kMaxDepth);
+    symbols = backtrace_symbols(stack, depth);
+    VerifyOrExit(symbols != nullptr);
+
+    for (int i = 0; i < depth; i++)
+    {
+        demangleSymbol(i, symbols[i]);
+    }
+
+    free(symbols);
+
+exit:
+    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)
+{
+    OT_UNUSED_VARIABLE(ucontext);
+    OT_UNUSED_VARIABLE(info);
+
+    otLogCritPlat("------------------ BEGINNING OF CRASH -------------");
+    otLogCritPlat("*** FATAL ERROR: Caught signal: %d (%s)", sig, strsignal(sig));
+
+    dumpStack();
+
+    otLogCritPlat("------------------ END OF CRASH ------------------");
+
+    resetSignalActions();
+    raise(sig);
+}
+
+void platformBacktraceInit(void)
+{
+    struct sigaction sigact;
+
+    sigact.sa_sigaction = &signalCritical;
+    sigact.sa_flags     = SA_RESTART | SA_SIGINFO | SA_NOCLDWAIT;
+
+    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) {}
+#endif // OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE || defined(__GLIBC__)
+#endif // OPENTHREAD_POSIX_CONFIG_BACKTRACE_ENABLE
diff --git a/src/posix/platform/config_file.cpp b/src/posix/platform/config_file.cpp
new file mode 100644
index 0000000..dd78c97
--- /dev/null
+++ b/src/posix/platform/config_file.cpp
@@ -0,0 +1,215 @@
+/*
+ *  Copyright (c) 2022, 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 "config_file.hpp"
+
+#include <libgen.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "utils.hpp"
+#include <openthread/logging.h>
+#include "common/code_utils.hpp"
+#include "lib/platform/exit_code.h"
+
+namespace ot {
+namespace Posix {
+
+ConfigFile::ConfigFile(const char *aFilePath)
+    : mFilePath(aFilePath)
+{
+    assert(mFilePath != nullptr);
+    VerifyOrDie(strlen(mFilePath) + strlen(kSwapSuffix) < kFileNameMaxSize, OT_EXIT_FAILURE);
+}
+
+otError ConfigFile::Get(const char *aKey, int &aIterator, char *aValue, int aValueLength)
+{
+    otError  error = OT_ERROR_NONE;
+    char     line[kLineMaxSize + 1];
+    FILE    *fp = nullptr;
+    char    *ret;
+    long int pos;
+
+    VerifyOrExit((aKey != nullptr) && (aValue != nullptr), error = OT_ERROR_INVALID_ARGS);
+    VerifyOrExit((fp = fopen(mFilePath, "r")) != nullptr, error = OT_ERROR_NOT_FOUND);
+    VerifyOrDie(fseek(fp, aIterator, SEEK_SET) == 0, OT_EXIT_ERROR_ERRNO);
+
+    while ((ret = fgets(line, sizeof(line), fp)) != nullptr)
+    {
+        char *str;
+        char *key;
+        char *value;
+
+        // If the string exceeds the `sizeof(line) - 1`, the string will be truncated to `sizeof(line) - 1` bytes string
+        // by the function `fgets()`.
+        if (strlen(line) + 1 == sizeof(line))
+        {
+            // The line is too long.
+            continue;
+        }
+
+        // Remove comments
+        strtok(line, kCommentDelimiter);
+
+        if ((str = strstr(line, "=")) == nullptr)
+        {
+            continue;
+        }
+
+        *str = '\0';
+        key  = line;
+
+        Strip(key);
+
+        if (strcmp(aKey, key) == 0)
+        {
+            value = str + 1;
+            Strip(value);
+            aValueLength = OT_MIN(static_cast<int>(strlen(value)), (aValueLength - 1));
+            memcpy(aValue, value, static_cast<size_t>(aValueLength));
+            aValue[aValueLength] = '\0';
+            break;
+        }
+    }
+
+    VerifyOrExit(ret != nullptr, error = OT_ERROR_NOT_FOUND);
+    VerifyOrDie((pos = ftell(fp)) >= 0, OT_EXIT_ERROR_ERRNO);
+    aIterator = static_cast<int>(pos);
+
+exit:
+    if (fp != nullptr)
+    {
+        fclose(fp);
+    }
+
+    return error;
+}
+
+otError ConfigFile::Add(const char *aKey, const char *aValue)
+{
+    otError     error = OT_ERROR_NONE;
+    FILE       *fp    = nullptr;
+    char       *path  = nullptr;
+    char       *dir;
+    struct stat st;
+
+    VerifyOrExit((aKey != nullptr) && (aValue != nullptr), error = OT_ERROR_INVALID_ARGS);
+    VerifyOrDie((path = strdup(mFilePath)) != nullptr, OT_EXIT_ERROR_ERRNO);
+    dir = dirname(path);
+
+    if (stat(dir, &st) == -1)
+    {
+        VerifyOrDie(mkdir(dir, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) == 0, OT_EXIT_ERROR_ERRNO);
+    }
+
+    VerifyOrDie((fp = fopen(mFilePath, "at")) != NULL, OT_EXIT_ERROR_ERRNO);
+    VerifyOrDie(fprintf(fp, "%s=%s\n", aKey, aValue) > 0, OT_EXIT_ERROR_ERRNO);
+
+exit:
+    if (fp != nullptr)
+    {
+        fclose(fp);
+    }
+
+    if (path != nullptr)
+    {
+        free(path);
+    }
+
+    return error;
+}
+
+otError ConfigFile::Clear(const char *aKey)
+{
+    otError error = OT_ERROR_NONE;
+    char    swapFile[kFileNameMaxSize];
+    char    line[kLineMaxSize];
+    FILE   *fp     = nullptr;
+    FILE   *fpSwap = nullptr;
+
+    VerifyOrExit(aKey != nullptr, error = OT_ERROR_INVALID_ARGS);
+    VerifyOrDie((fp = fopen(mFilePath, "r")) != NULL, OT_EXIT_ERROR_ERRNO);
+    snprintf(swapFile, sizeof(swapFile), "%s%s", mFilePath, kSwapSuffix);
+    VerifyOrDie((fpSwap = fopen(swapFile, "w+")) != NULL, OT_EXIT_ERROR_ERRNO);
+
+    while (fgets(line, sizeof(line), fp) != nullptr)
+    {
+        bool  containsKey;
+        char *str1;
+        char *str2;
+
+        str1 = strstr(line, kCommentDelimiter);
+        str2 = strstr(line, aKey);
+
+        // If only the comment contains the key string, ignore it.
+        containsKey = (str2 != nullptr) && (str1 == nullptr || str2 < str1);
+
+        if (!containsKey)
+        {
+            fputs(line, fpSwap);
+        }
+    }
+
+exit:
+    if (fp != nullptr)
+    {
+        fclose(fp);
+    }
+
+    if (fpSwap != nullptr)
+    {
+        fclose(fpSwap);
+    }
+
+    if (error == OT_ERROR_NONE)
+    {
+        VerifyOrDie(rename(swapFile, mFilePath) == 0, OT_EXIT_ERROR_ERRNO);
+    }
+
+    return error;
+}
+
+void ConfigFile::Strip(char *aString)
+{
+    int count = 0;
+
+    for (int i = 0; aString[i]; i++)
+    {
+        if (aString[i] != ' ' && aString[i] != '\r' && aString[i] != '\n')
+        {
+            aString[count++] = aString[i];
+        }
+    }
+
+    aString[count] = '\0';
+}
+} // namespace Posix
+} // namespace ot
diff --git a/src/posix/platform/config_file.hpp b/src/posix/platform/config_file.hpp
new file mode 100644
index 0000000..c0a74ca
--- /dev/null
+++ b/src/posix/platform/config_file.hpp
@@ -0,0 +1,107 @@
+/*
+ *  Copyright (c) 2022, 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.
+ */
+
+#ifndef POSIX_PLATFORM_CONFIG_FILE_HPP_
+#define POSIX_PLATFORM_CONFIG_FILE_HPP_
+
+#include <assert.h>
+#include <stdint.h>
+#include <openthread/error.h>
+
+namespace ot {
+namespace Posix {
+
+/**
+ * This class provides read/write/clear methods for key/value configuration files.
+ *
+ */
+class ConfigFile
+{
+public:
+    /**
+     * This method initializes the configuration file path.
+     *
+     * @param[in]  aFilePath  A pointer to the null-terminated file path.
+     *
+     */
+    explicit ConfigFile(const char *aFilePath);
+
+    /**
+     * This method gets a configuration from the configuration file.
+     *
+     * @param[in]      aKey          The key string associated with the requested configuration.
+     * @param[in,out]  aIterator     A reference to an iterator. MUST be initialized to 0 or the behavior is undefined.
+     * @param[out]     aValue        A pointer to where the new value string of the configuration should be read from.
+     *                               The @p aValue string will be terminated with `\0` if this method returns success.
+     * @param[in]      aValueLength  The max length of the data pointed to by @p aValue.
+     *
+     * @retval OT_ERROR_NONE          The given configuration was found and fetched successfully.
+     * @retval OT_ERROR_NOT_FOUND     The given key or iterator was not found in the configuration file.
+     * @retval OT_ERROR_INVALID_ARGS  If @p aKey or @p aValue was NULL.
+     *
+     */
+    otError Get(const char *aKey, int &aIterator, char *aValue, int aValueLength);
+
+    /**
+     * This method adds a configuration to the configuration file.
+     *
+     * @param[in]  aKey    The key string associated with the requested configuration.
+     * @param[in]  aValue  A pointer to where the new value string of the configuration should be written.
+     *
+     * @retval OT_ERROR_NONE          The given key was found and removed successfully.
+     * @retval OT_ERROR_INVALID_ARGS  If @p aKey or @p aValue was NULL.
+     *
+     */
+    otError Add(const char *aKey, const char *aValue);
+
+    /**
+     * This function removes all configurations with the same key string from the configuration file.
+     *
+     * @param[in]  aKey  The key string associated with the requested configuration.
+     *
+     * @retval OT_ERROR_NONE          The given key was found and removed successfully.
+     * @retval OT_ERROR_INVALID_ARGS  If @p aKey was NULL.
+     *
+     */
+    otError Clear(const char *aKey);
+
+private:
+    const char               *kCommentDelimiter = "#";
+    const char               *kSwapSuffix       = ".swap";
+    static constexpr uint16_t kLineMaxSize      = 512;
+    static constexpr uint16_t kFileNameMaxSize  = 255;
+
+    void Strip(char *aString);
+
+    const char *mFilePath;
+};
+
+} // namespace Posix
+} // namespace ot
+
+#endif // POSIX_PLATFORM_CONFIG_FILE_HPP_
diff --git a/src/posix/platform/daemon.cpp b/src/posix/platform/daemon.cpp
index 275b5a8..1333d33 100644
--- a/src/posix/platform/daemon.cpp
+++ b/src/posix/platform/daemon.cpp
@@ -60,9 +60,10 @@
 
 void GetFilename(Filename &aFilename, const char *aPattern)
 {
-    int rval;
+    int         rval;
+    const char *netIfName = strlen(gNetifName) > 0 ? gNetifName : OPENTHREAD_POSIX_CONFIG_THREAD_NETIF_DEFAULT_NAME;
 
-    rval = snprintf(aFilename, sizeof(aFilename), aPattern, gNetifName);
+    rval = snprintf(aFilename, sizeof(aFilename), aPattern, netIfName);
     if (rval < 0 && static_cast<size_t>(rval) >= sizeof(aFilename))
     {
         DieNow(OT_EXIT_INVALID_ARGUMENTS);
@@ -71,6 +72,18 @@
 
 } // namespace
 
+int Daemon::OutputFormat(const char *aFormat, ...)
+{
+    int     ret;
+    va_list ap;
+
+    va_start(ap, aFormat);
+    ret = OutputFormatV(aFormat, ap);
+    va_end(ap);
+
+    return ret;
+}
+
 int Daemon::OutputFormatV(const char *aFormat, va_list aArguments)
 {
     char buf[OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH + 1];
@@ -145,7 +158,7 @@
     }
     else
     {
-        otLogInfoPlat("Session socket is ready", strerror(errno));
+        otLogInfoPlat("Session socket is ready");
     }
 }
 
@@ -236,12 +249,14 @@
         DieNowWithMessage("listen", OT_EXIT_ERROR_ERRNO);
     }
 
+#if OPENTHREAD_POSIX_CONFIG_DAEMON_CLI_ENABLE
     otCliInit(
         gInstance,
         [](void *aContext, const char *aFormat, va_list aArguments) -> int {
             return static_cast<Daemon *>(aContext)->OutputFormatV(aFormat, aArguments);
         },
         this);
+#endif
 
     Mainloop::Manager::Get().Add(*this);
 
@@ -341,8 +356,11 @@
         if (rval > 0)
         {
             buffer[rval] = '\0';
-            otLogInfoPlat("> %s", reinterpret_cast<const char *>(buffer));
+#if OPENTHREAD_POSIX_CONFIG_DAEMON_CLI_ENABLE
             otCliInputLine(reinterpret_cast<char *>(buffer));
+#else
+            OutputFormat("Error: CLI is disabled!\n");
+#endif
         }
         else
         {
diff --git a/src/posix/platform/daemon.hpp b/src/posix/platform/daemon.hpp
index 213ddf2..4fdd3b3 100644
--- a/src/posix/platform/daemon.hpp
+++ b/src/posix/platform/daemon.hpp
@@ -47,6 +47,7 @@
     void Process(const otSysMainloopContext &aContext) override;
 
 private:
+    int  OutputFormat(const char *aFormat, ...);
     int  OutputFormatV(const char *aFormat, va_list aArguments);
     void InitializeSessionSocket(void);
 
diff --git a/src/posix/platform/entropy.cpp b/src/posix/platform/entropy.cpp
index 561f279..79f4938 100644
--- a/src/posix/platform/entropy.cpp
+++ b/src/posix/platform/entropy.cpp
@@ -95,7 +95,7 @@
 
 #if __SANITIZE_ADDRESS__ == 0
 
-    FILE * file = nullptr;
+    FILE  *file = nullptr;
     size_t readLength;
 
     VerifyOrExit(aOutput && aOutputLength, error = OT_ERROR_INVALID_ARGS);
diff --git a/src/posix/platform/hdlc_interface.cpp b/src/posix/platform/hdlc_interface.cpp
index ffbc206..7390c89 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__
 
@@ -125,8 +126,8 @@
 namespace Posix {
 
 HdlcInterface::HdlcInterface(SpinelInterface::ReceiveFrameCallback aCallback,
-                             void *                                aCallbackContext,
-                             SpinelInterface::RxFrameBuffer &      aFrameBuffer)
+                             void                                 *aCallbackContext,
+                             SpinelInterface::RxFrameBuffer       &aFrameBuffer)
     : mReceiveFrameCallback(aCallback)
     , mReceiveFrameContext(aCallbackContext)
     , mReceiveFrameBuffer(aFrameBuffer)
@@ -139,11 +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;
@@ -177,15 +173,9 @@
     return error;
 }
 
-HdlcInterface::~HdlcInterface(void)
-{
-    Deinit();
-}
+HdlcInterface::~HdlcInterface(void) { Deinit(); }
 
-void HdlcInterface::Deinit(void)
-{
-    CloseFile();
-}
+void HdlcInterface::Deinit(void) { CloseFile(); }
 
 void HdlcInterface::Read(void)
 {
@@ -204,10 +194,7 @@
     }
 }
 
-void HdlcInterface::Decode(const uint8_t *aBuffer, uint16_t aLength)
-{
-    mHdlcDecoder.Decode(aBuffer, aLength);
-}
+void HdlcInterface::Decode(const uint8_t *aBuffer, uint16_t aLength) { mHdlcDecoder.Decode(aBuffer, aLength); }
 
 otError HdlcInterface::SendFrame(const uint8_t *aFrame, uint16_t aLength)
 {
@@ -222,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;
 }
 
@@ -341,25 +334,44 @@
     return error;
 }
 
-void HdlcInterface::UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, int &aMaxFd, struct timeval &aTimeout)
+void HdlcInterface::UpdateFdSet(void *aMainloopContext)
 {
-    OT_UNUSED_VARIABLE(aWriteFdSet);
-    OT_UNUSED_VARIABLE(aTimeout);
+    otSysMainloopContext *context = reinterpret_cast<otSysMainloopContext *>(aMainloopContext);
 
-    FD_SET(mSockFd, &aReadFdSet);
+    assert(context != nullptr);
 
-    if (aMaxFd < mSockFd)
+    FD_SET(mSockFd, &context->mReadFdSet);
+
+    if (context->mMaxFd < mSockFd)
     {
-        aMaxFd = mSockFd;
+        context->mMaxFd = mSockFd;
     }
 }
 
-void HdlcInterface::Process(const RadioProcessContext &aContext)
+void HdlcInterface::Process(const void *aMainloopContext)
 {
-    if (FD_ISSET(mSockFd, aContext.mReadFdSet))
+#if OPENTHREAD_POSIX_VIRTUAL_TIME
+    /**
+     * Process read data (decode the data).
+     *
+     * Is intended only for virtual time simulation. Its behavior is similar to `Read()` but instead of
+     * reading the data from the radio socket, it uses the given data in @p `event`.
+     */
+    const VirtualTimeEvent *event = reinterpret_cast<const VirtualTimeEvent *>(aMainloopContext);
+
+    assert(event != nullptr);
+
+    Decode(event->mData, event->mDataLength);
+#else
+    const otSysMainloopContext *context = reinterpret_cast<const otSysMainloopContext *>(aMainloopContext);
+
+    assert(context != nullptr);
+
+    if (FD_ISSET(mSockFd, &context->mReadFdSet))
     {
         Read();
     }
+#endif
 }
 
 otError HdlcInterface::WaitForWritable(void)
@@ -437,7 +449,7 @@
     if (isatty(fd))
     {
         struct termios tios;
-        const char *   value;
+        const char    *value;
         speed_t        speed;
 
         int      stopBit  = 1;
@@ -630,7 +642,7 @@
     if (0 == pid)
     {
         constexpr int kMaxArguments = 32;
-        char *        argv[kMaxArguments + 1];
+        char         *argv[kMaxArguments + 1];
         size_t        index = 0;
 
         argv[index++] = const_cast<char *>(aRadioUrl.GetPath());
diff --git a/src/posix/platform/hdlc_interface.hpp b/src/posix/platform/hdlc_interface.hpp
index 4fa2ba0..1c69e7c 100644
--- a/src/posix/platform/hdlc_interface.hpp
+++ b/src/posix/platform/hdlc_interface.hpp
@@ -40,8 +40,6 @@
 #include "lib/spinel/openthread-spinel-config.h"
 #include "lib/spinel/spinel_interface.hpp"
 
-#if OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_UART
-
 namespace ot {
 namespace Posix {
 
@@ -49,7 +47,7 @@
  * This class defines an HDLC interface to the Radio Co-processor (RCP)
  *
  */
-class HdlcInterface
+class HdlcInterface : public ot::Spinel::SpinelInterface
 {
 public:
     /**
@@ -61,8 +59,8 @@
      *
      */
     HdlcInterface(Spinel::SpinelInterface::ReceiveFrameCallback aCallback,
-                  void *                                        aCallbackContext,
-                  Spinel::SpinelInterface::RxFrameBuffer &      aFrameBuffer);
+                  void                                         *aCallbackContext,
+                  Spinel::SpinelInterface::RxFrameBuffer       &aFrameBuffer);
 
     /**
      * This destructor deinitializes the object.
@@ -120,34 +118,18 @@
     /**
      * This method updates the file descriptor sets with file descriptors used by the radio driver.
      *
-     * @param[in,out]  aReadFdSet   A reference to the read file descriptors.
-     * @param[in,out]  aWriteFdSet  A reference to the write file descriptors.
-     * @param[in,out]  aMaxFd       A reference to the max file descriptor.
-     * @param[in,out]  aTimeout     A reference to the timeout.
+     * @param[in,out]   aMainloopContext  A pointer to the mainloop context containing fd_sets.
      *
      */
-    void UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, int &aMaxFd, struct timeval &aTimeout);
+    void UpdateFdSet(void *aMainloopContext);
 
     /**
      * This method performs radio driver processing.
      *
-     * @param[in]   aContext        The context containing fd_sets.
+     * @param[in]   aMainloopContext  A pointer to the mainloop context containing fd_sets.
      *
      */
-    void Process(const RadioProcessContext &aContext);
-
-#if OPENTHREAD_POSIX_VIRTUAL_TIME
-    /**
-     * This method process read data (decode the data).
-     *
-     * This method is intended only for virtual time simulation. Its behavior is similar to `Read()` but instead of
-     * reading the data from the radio socket, it uses the given data in @p `aEvent`.
-     *
-     * @param[in] aEvent   The data event.
-     *
-     */
-    void Process(const VirtualTimeEvent &aEvent) { Decode(aEvent.mData, aEvent.mDataLength); }
-#endif
+    void Process(const void *aMainloopContext);
 
     /**
      * This method returns the bus speed between the host and the radio.
@@ -158,16 +140,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 +158,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
@@ -258,8 +243,8 @@
     };
 
     Spinel::SpinelInterface::ReceiveFrameCallback mReceiveFrameCallback;
-    void *                                        mReceiveFrameContext;
-    Spinel::SpinelInterface::RxFrameBuffer &      mReceiveFrameBuffer;
+    void                                         *mReceiveFrameContext;
+    Spinel::SpinelInterface::RxFrameBuffer       &mReceiveFrameBuffer;
 
     int             mSockFd;
     uint32_t        mBaudRate;
@@ -275,6 +260,4 @@
 
 } // namespace Posix
 } // namespace ot
-
-#endif // OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_UART
 #endif // POSIX_APP_HDLC_INTERFACE_HPP_
diff --git a/src/posix/platform/include/openthread/openthread-system.h b/src/posix/platform/include/openthread/openthread-system.h
index 9a1a666..35ae715 100644
--- a/src/posix/platform/include/openthread/openthread-system.h
+++ b/src/posix/platform/include/openthread/openthread-system.h
@@ -80,6 +80,7 @@
     uint8_t     mRadioUrlNum;                                  ///< Number of Radio URLs.
     int         mRealTimeSignal;                               ///< The real-time signal for microsecond timer.
     uint32_t    mSpeedUpFactor;                                ///< Speed up factor.
+    bool        mPersistentInterface;                          ///< Whether persistent the interface
     bool        mDryRun;                                       ///< If 'DryRun' is set, the posix daemon will exit
                                                                ///< directly after initialization.
 } otPlatformConfig;
@@ -218,6 +219,29 @@
  */
 const otRcpInterfaceMetrics *otSysGetRcpInterfaceMetrics(void);
 
+/**
+ * This function returns the ifr_flags of the infrastructure network interface.
+ *
+ * @returns The ifr_flags of infrastructure network interface.
+ *
+ */
+uint32_t otSysGetInfraNetifFlags(void);
+
+typedef struct otSysInfraNetIfAddressCounters
+{
+    uint32_t mLinkLocalAddresses;
+    uint32_t mUniqueLocalAddresses;
+    uint32_t mGlobalUnicastAddresses;
+} otSysInfraNetIfAddressCounters;
+
+/**
+ * This functions counts the number of addresses on the infrastructure network interface.
+ *
+ * @param[out] aAddressCounters  The counters of addresses on infrastructure network interface.
+ *
+ */
+void otSysCountInfraNetifAddresses(otSysInfraNetIfAddressCounters *aAddressCounters);
+
 #ifdef __cplusplus
 } // end of extern "C"
 #endif
diff --git a/src/posix/platform/include/openthread/platform/secure_settings.h b/src/posix/platform/include/openthread/platform/secure_settings.h
index d206c26..288ad4f 100644
--- a/src/posix/platform/include/openthread/platform/secure_settings.h
+++ b/src/posix/platform/include/openthread/platform/secure_settings.h
@@ -97,8 +97,8 @@
 otError otPosixSecureSettingsGet(otInstance *aInstance,
                                  uint16_t    aKey,
                                  int         aIndex,
-                                 uint8_t *   aValue,
-                                 uint16_t *  aValueLength);
+                                 uint8_t    *aValue,
+                                 uint16_t   *aValueLength);
 
 /**
  * This function sets or replaces the value of a setting identified by aKey. If there was more than one value
diff --git a/src/posix/platform/infra_if.cpp b/src/posix/platform/infra_if.cpp
index 11f1562..2b37cef 100644
--- a/src/posix/platform/infra_if.cpp
+++ b/src/posix/platform/infra_if.cpp
@@ -33,7 +33,7 @@
 
 #include "platform-posix.h"
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#if OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
 
 #ifdef __APPLE__
 #define __APPLE_USE_RFC_3542
@@ -41,10 +41,12 @@
 
 #include <errno.h>
 #include <ifaddrs.h>
+#include <netdb.h>
 // clang-format off
 #include <netinet/in.h>
 #include <netinet/icmp6.h>
 // clang-format on
+#include <signal.h>
 #include <sys/ioctl.h>
 #include <sys/types.h>
 #include <unistd.h>
@@ -90,20 +92,32 @@
 
 otError otPlatInfraIfSendIcmp6Nd(uint32_t            aInfraIfIndex,
                                  const otIp6Address *aDestAddress,
-                                 const uint8_t *     aBuffer,
+                                 const uint8_t      *aBuffer,
                                  uint16_t            aBufferLength)
 {
     return ot::Posix::InfraNetif::Get().SendIcmp6Nd(aInfraIfIndex, *aDestAddress, aBuffer, aBufferLength);
 }
 
-bool platformInfraIfIsRunning(void)
+otError otPlatInfraIfDiscoverNat64Prefix(uint32_t aInfraIfIndex)
 {
-    return ot::Posix::InfraNetif::Get().IsRunning();
+    OT_UNUSED_VARIABLE(aInfraIfIndex);
+
+#if OPENTHREAD_POSIX_CONFIG_NAT64_AIL_PREFIX_ENABLE
+    return ot::Posix::InfraNetif::Get().DiscoverNat64Prefix(aInfraIfIndex);
+#else
+    return OT_ERROR_DROP;
+#endif
 }
 
-const char *otSysGetInfraNetifName(void)
+bool platformInfraIfIsRunning(void) { return ot::Posix::InfraNetif::Get().IsRunning(); }
+
+const char *otSysGetInfraNetifName(void) { return ot::Posix::InfraNetif::Get().GetNetifName(); }
+
+uint32_t otSysGetInfraNetifFlags(void) { return ot::Posix::InfraNetif::Get().GetFlags(); }
+
+void otSysCountInfraNetifAddresses(otSysInfraNetIfAddressCounters *aAddressCounters)
 {
-    return ot::Posix::InfraNetif::Get().GetNetifName();
+    ot::Posix::InfraNetif::Get().CountAddresses(*aAddressCounters);
 }
 
 namespace ot {
@@ -123,10 +137,11 @@
     sock = SocketWithCloseExec(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6, kSocketBlock);
     VerifyOrDie(sock != -1, OT_EXIT_ERROR_ERRNO);
 
-    // Only accept router advertisements and solicitations.
+    // Only accept Router Advertisements, Router Solicitations and Neighbor Advertisements.
     ICMP6_FILTER_SETBLOCKALL(&filter);
     ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filter);
     ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filter);
+    ICMP6_FILTER_SETPASS(ND_NEIGHBOR_ADVERT, &filter);
 
     rval = setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, &filter, sizeof(filter));
     VerifyOrDie(rval == 0, OT_EXIT_ERROR_ERRNO);
@@ -155,6 +170,15 @@
     return sock;
 }
 
+bool IsAddressLinkLocal(const in6_addr &aAddress)
+{
+    return ((aAddress.s6_addr[0] & 0xff) == 0xfe) && ((aAddress.s6_addr[1] & 0xc0) == 0x80);
+}
+
+bool IsAddressUniqueLocal(const in6_addr &aAddress) { return (aAddress.s6_addr[0] & 0xfe) == 0xfc; }
+
+bool IsAddressGlobalUnicast(const in6_addr &aAddress) { return (aAddress.s6_addr[0] & 0xe0) == 0x20; }
+
 // Create a net-link socket that subscribes to link & addresses events.
 int CreateNetLinkSocket(void)
 {
@@ -179,7 +203,7 @@
 
 otError InfraNetif::SendIcmp6Nd(uint32_t            aInfraIfIndex,
                                 const otIp6Address &aDestAddress,
-                                const uint8_t *     aBuffer,
+                                const uint8_t      *aBuffer,
                                 uint16_t            aBufferLength)
 {
     otError error = OT_ERROR_NONE;
@@ -190,7 +214,7 @@
     int                 hopLimit = 255;
     uint8_t             cmsgBuffer[CMSG_SPACE(sizeof(*packetInfo)) + CMSG_SPACE(sizeof(hopLimit))];
     struct msghdr       msgHeader;
-    struct cmsghdr *    cmsgPointer;
+    struct cmsghdr     *cmsgPointer;
     ssize_t             rval;
     struct sockaddr_in6 dest;
 
@@ -251,7 +275,9 @@
     return error;
 }
 
-bool InfraNetif::IsRunning(void) const
+bool InfraNetif::IsRunning(void) const { return (GetFlags() & IFF_RUNNING) && HasLinkLocalAddress(); }
+
+uint32_t InfraNetif::GetFlags(void) const
 {
     int          sock;
     struct ifreq ifReq;
@@ -269,7 +295,42 @@
 
     close(sock);
 
-    return (ifReq.ifr_flags & IFF_RUNNING) && HasLinkLocalAddress();
+    return static_cast<uint32_t>(ifReq.ifr_flags);
+}
+
+void InfraNetif::CountAddresses(otSysInfraNetIfAddressCounters &aAddressCounters) const
+{
+    struct ifaddrs *ifAddrs = nullptr;
+
+    aAddressCounters.mLinkLocalAddresses     = 0;
+    aAddressCounters.mUniqueLocalAddresses   = 0;
+    aAddressCounters.mGlobalUnicastAddresses = 0;
+
+    if (getifaddrs(&ifAddrs) < 0)
+    {
+        otLogWarnPlat("failed to get netif addresses: %s", strerror(errno));
+        ExitNow();
+    }
+
+    for (struct ifaddrs *addr = ifAddrs; addr != nullptr; addr = addr->ifa_next)
+    {
+        in6_addr *in6Addr;
+
+        if (strncmp(addr->ifa_name, mInfraIfName, sizeof(mInfraIfName)) != 0 || addr->ifa_addr->sa_family != AF_INET6)
+        {
+            continue;
+        }
+
+        in6Addr = &(reinterpret_cast<sockaddr_in6 *>(addr->ifa_addr)->sin6_addr);
+        aAddressCounters.mLinkLocalAddresses += IsAddressLinkLocal(*in6Addr);
+        aAddressCounters.mUniqueLocalAddresses += IsAddressUniqueLocal(*in6Addr);
+        aAddressCounters.mGlobalUnicastAddresses += IsAddressGlobalUnicast(*in6Addr);
+    }
+
+    freeifaddrs(ifAddrs);
+
+exit:
+    return;
 }
 
 bool InfraNetif::HasLinkLocalAddress(void) const
@@ -515,6 +576,135 @@
     }
 }
 
+#if OPENTHREAD_POSIX_CONFIG_NAT64_AIL_PREFIX_ENABLE
+const char         InfraNetif::kWellKnownIpv4OnlyName[]   = "ipv4only.arpa";
+const otIp4Address InfraNetif::kWellKnownIpv4OnlyAddress1 = {{{192, 0, 0, 170}}};
+const otIp4Address InfraNetif::kWellKnownIpv4OnlyAddress2 = {{{192, 0, 0, 171}}};
+const uint8_t      InfraNetif::kValidNat64PrefixLength[]  = {96, 64, 56, 48, 40, 32};
+
+void InfraNetif::DiscoverNat64PrefixDone(union sigval sv)
+{
+    struct gaicb    *req = (struct gaicb *)sv.sival_ptr;
+    struct addrinfo *res = (struct addrinfo *)req->ar_result;
+
+    otIp6Prefix prefix = {};
+
+    VerifyOrExit((char *)req->ar_name == kWellKnownIpv4OnlyName);
+
+    otLogInfoPlat("Handling host address response for %s", kWellKnownIpv4OnlyName);
+
+    // We extract the first valid NAT64 prefix from the address look-up response.
+    for (struct addrinfo *rp = res; rp != NULL && prefix.mLength == 0; rp = rp->ai_next)
+    {
+        struct sockaddr_in6 *ip6Addr;
+        otIp6Address         ip6Address;
+
+        if (rp->ai_family != AF_INET6)
+        {
+            continue;
+        }
+
+        ip6Addr = reinterpret_cast<sockaddr_in6 *>(rp->ai_addr);
+        memcpy(&ip6Address.mFields.m8, &ip6Addr->sin6_addr.s6_addr, OT_IP6_ADDRESS_SIZE);
+        for (uint8_t length : kValidNat64PrefixLength)
+        {
+            otIp4Address ip4Address;
+
+            otIp4ExtractFromIp6Address(length, &ip6Address, &ip4Address);
+            if (otIp4IsAddressEqual(&ip4Address, &kWellKnownIpv4OnlyAddress1) ||
+                otIp4IsAddressEqual(&ip4Address, &kWellKnownIpv4OnlyAddress2))
+            {
+                // We check that the well-known IPv4 address is present only once in the IPv6 address.
+                // In case another instance of the value is found for another prefix length, we ignore this address
+                // and search for the other well-known IPv4 address (per RFC 7050 section 3).
+                bool foundDuplicate = false;
+
+                for (uint8_t dupLength : kValidNat64PrefixLength)
+                {
+                    otIp4Address dupIp4Address;
+
+                    if (dupLength == length)
+                    {
+                        continue;
+                    }
+
+                    otIp4ExtractFromIp6Address(dupLength, &ip6Address, &dupIp4Address);
+                    if (otIp4IsAddressEqual(&dupIp4Address, &ip4Address))
+                    {
+                        foundDuplicate = true;
+                        break;
+                    }
+                }
+
+                if (!foundDuplicate)
+                {
+                    otIp6GetPrefix(&ip6Address, length, &prefix);
+                    break;
+                }
+            }
+
+            if (prefix.mLength != 0)
+            {
+                break;
+            }
+        }
+    }
+
+    otPlatInfraIfDiscoverNat64PrefixDone(gInstance, Get().mInfraIfIndex, &prefix);
+
+exit:
+    freeaddrinfo(res);
+    freeaddrinfo((struct addrinfo *)req->ar_request);
+    free(req);
+}
+
+otError InfraNetif::DiscoverNat64Prefix(uint32_t aInfraIfIndex)
+{
+    otError          error = OT_ERROR_NONE;
+    struct addrinfo *hints = nullptr;
+    struct gaicb    *reqs[1];
+    struct sigevent  sig;
+    int              status;
+
+    VerifyOrExit(aInfraIfIndex == mInfraIfIndex, error = OT_ERROR_DROP);
+    hints = (struct addrinfo *)malloc(sizeof(struct addrinfo));
+    VerifyOrExit(hints != nullptr, error = OT_ERROR_NO_BUFS);
+    memset(hints, 0, sizeof(struct addrinfo));
+    hints->ai_family   = AF_INET6;
+    hints->ai_socktype = SOCK_STREAM;
+
+    reqs[0] = (struct gaicb *)malloc(sizeof(struct gaicb));
+    VerifyOrExit(reqs[0] != nullptr, error = OT_ERROR_NO_BUFS);
+    memset(reqs[0], 0, sizeof(struct gaicb));
+    reqs[0]->ar_name    = kWellKnownIpv4OnlyName;
+    reqs[0]->ar_request = hints;
+
+    memset(&sig, 0, sizeof(struct sigevent));
+    sig.sigev_notify          = SIGEV_THREAD;
+    sig.sigev_value.sival_ptr = reqs[0];
+    sig.sigev_notify_function = &InfraNetif::DiscoverNat64PrefixDone;
+
+    status = getaddrinfo_a(GAI_NOWAIT, reqs, 1, &sig);
+
+    if (status != 0)
+    {
+        otLogNotePlat("getaddrinfo_a failed: %s", gai_strerror(status));
+        ExitNow(error = OT_ERROR_FAILED);
+    }
+    otLogInfoPlat("getaddrinfo_a requested for %s", kWellKnownIpv4OnlyName);
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        if (hints)
+        {
+            freeaddrinfo(hints);
+        }
+        free(reqs[0]);
+    }
+    return error;
+}
+#endif // OPENTHREAD_POSIX_CONFIG_NAT64_AIL_PREFIX_ENABLE
+
 void InfraNetif::Process(const otSysMainloopContext &aContext)
 {
     VerifyOrExit(mInfraIfIcmp6Socket != -1);
@@ -543,4 +733,4 @@
 
 } // namespace Posix
 } // namespace ot
-#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#endif // OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
diff --git a/src/posix/platform/infra_if.hpp b/src/posix/platform/infra_if.hpp
index ccd2b12..03146fa 100644
--- a/src/posix/platform/infra_if.hpp
+++ b/src/posix/platform/infra_if.hpp
@@ -34,11 +34,13 @@
 #include "openthread-posix-config.h"
 
 #include <net/if.h>
+#include <openthread/nat64.h>
+#include <openthread/openthread-system.h>
 
 #include "core/common/non_copyable.hpp"
 #include "posix/platform/mainloop.hpp"
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#if OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
 
 namespace ot {
 namespace Posix {
@@ -107,6 +109,22 @@
     bool IsRunning(void) const;
 
     /**
+     * This method returns the ifr_flags of the infrastructure network interface.
+     *
+     * @returns The ifr_flags of the infrastructure network interface.
+     *
+     */
+    uint32_t GetFlags(void) const;
+
+    /**
+     * This functions counts the number of addresses on the infrastructure network interface.
+     *
+     * @param[out] aAddressCounters  The counters of addresses on infrastructure network interface.
+     *
+     */
+    void CountAddresses(otSysInfraNetIfAddressCounters &aAddressCounters) const;
+
+    /**
      * This method sends an ICMPv6 Neighbor Discovery message on given infrastructure interface.
      *
      * See RFC 4861: https://tools.ietf.org/html/rfc4861.
@@ -126,10 +144,22 @@
      */
     otError SendIcmp6Nd(uint32_t            aInfraIfIndex,
                         const otIp6Address &aDestAddress,
-                        const uint8_t *     aBuffer,
+                        const uint8_t      *aBuffer,
                         uint16_t            aBufferLength);
 
     /**
+     * This method sends an asynchronous address lookup for the well-known host name "ipv4only.arpa"
+     * to discover the NAT64 prefix.
+     *
+     * @param[in]  aInfraIfIndex  The index of the infrastructure interface the address look-up is sent to.
+     *
+     * @retval  OT_ERROR_NONE    Successfully request address look-up.
+     * @retval  OT_ERROR_FAILED  Failed to request address look-up.
+     *
+     */
+    otError DiscoverNat64Prefix(uint32_t aInfraIfIndex);
+
+    /**
      * This method gets the infrastructure network interface name.
      *
      * @returns The infrastructure network interface name, or `nullptr` if not specified.
@@ -146,16 +176,22 @@
     static InfraNetif &Get(void);
 
 private:
+    static const char         kWellKnownIpv4OnlyName[];   // "ipv4only.arpa"
+    static const otIp4Address kWellKnownIpv4OnlyAddress1; // 192.0.0.170
+    static const otIp4Address kWellKnownIpv4OnlyAddress2; // 192.0.0.171
+    static const uint8_t      kValidNat64PrefixLength[];
+
     char     mInfraIfName[IFNAMSIZ];
     uint32_t mInfraIfIndex       = 0;
     int      mInfraIfIcmp6Socket = -1;
     int      mNetLinkSocket      = -1;
 
-    void ReceiveNetLinkMessage(void);
-    void ReceiveIcmp6Message(void);
-    bool HasLinkLocalAddress(void) const;
+    void        ReceiveNetLinkMessage(void);
+    void        ReceiveIcmp6Message(void);
+    bool        HasLinkLocalAddress(void) const;
+    static void DiscoverNat64PrefixDone(union sigval sv);
 };
 
 } // namespace Posix
 } // namespace ot
-#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#endif // OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
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/memory.cpp b/src/posix/platform/memory.cpp
index 5132c3f..15d4a79 100644
--- a/src/posix/platform/memory.cpp
+++ b/src/posix/platform/memory.cpp
@@ -34,13 +34,9 @@
 #include <openthread/platform/memory.h>
 
 #if OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
-void *otPlatCAlloc(size_t aNum, size_t aSize)
-{
-    return calloc(aNum, aSize);
-}
+extern "C" {
+void *otPlatCAlloc(size_t aNum, size_t aSize) { return calloc(aNum, aSize); }
 
-void otPlatFree(void *aPtr)
-{
-    free(aPtr);
-}
+void otPlatFree(void *aPtr) { free(aPtr); }
+} // extern "C"
 #endif
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.cpp b/src/posix/platform/multicast_routing.cpp
index 6580782..9d71481 100644
--- a/src/posix/platform/multicast_routing.cpp
+++ b/src/posix/platform/multicast_routing.cpp
@@ -86,16 +86,16 @@
     Mainloop::Manager::Get().Remove(*this);
 }
 
-void MulticastRoutingManager::HandleBackboneMulticastListenerEvent(void *                                 aContext,
+void MulticastRoutingManager::HandleBackboneMulticastListenerEvent(void                                  *aContext,
                                                                    otBackboneRouterMulticastListenerEvent aEvent,
-                                                                   const otIp6Address *                   aAddress)
+                                                                   const otIp6Address                    *aAddress)
 {
     static_cast<MulticastRoutingManager *>(aContext)->HandleBackboneMulticastListenerEvent(
         aEvent, static_cast<const Ip6::Address &>(*aAddress));
 }
 
 void MulticastRoutingManager::HandleBackboneMulticastListenerEvent(otBackboneRouterMulticastListenerEvent aEvent,
-                                                                   const Ip6::Address &                   aAddress)
+                                                                   const Ip6::Address                    &aAddress)
 {
     switch (aEvent)
     {
@@ -309,6 +309,8 @@
     }
     else
     {
+        VerifyOrExit(!aSrcAddr.IsLinkLocal(), error = OT_ERROR_NONE);
+        VerifyOrExit(aSrcAddr.GetPrefix() != AsCoreType(otThreadGetMeshLocalPrefix(gInstance)), error = OT_ERROR_NONE);
         // Forward multicast traffic from Thread to Backbone if multicast scope > kRealmLocalScope
         // TODO: (MLR) allow scope configuration of outbound multicast routing
         if (aGroupAddr.GetScope() > Ip6::Address::kRealmLocalScope)
@@ -451,7 +453,7 @@
     }
     else
     {
-        otLogWarnPlat("MulticastRoutingManager: %s: SIOCGETSGCNT_IN6 %s => %s failed: %s", __FUNCTION__,
+        otLogDebgPlat("MulticastRoutingManager: %s: SIOCGETSGCNT_IN6 %s => %s failed: %s", __FUNCTION__,
                       aMfc.mSrcAddr.ToString().AsCString(), aMfc.mGroupAddr.ToString().AsCString(), strerror(errno));
     }
 
@@ -541,8 +543,8 @@
     mLastUseTime = otPlatTimeGet();
 }
 
-void MulticastRoutingManager::SaveMulticastForwardingCache(const Ip6::Address &              aSrcAddr,
-                                                           const Ip6::Address &              aGroupAddr,
+void MulticastRoutingManager::SaveMulticastForwardingCache(const Ip6::Address               &aSrcAddr,
+                                                           const Ip6::Address               &aGroupAddr,
                                                            MulticastRoutingManager::MifIndex aIif,
                                                            MulticastRoutingManager::MifIndex aOif)
 {
diff --git a/src/posix/platform/multicast_routing.hpp b/src/posix/platform/multicast_routing.hpp
index 72177d8..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.
     };
 
@@ -126,13 +126,13 @@
     void    RemoveMulticastForwardingCache(MulticastForwardingCache &aMfc) const;
     static const char *MifIndexToString(MifIndex aMif);
     void               DumpMulticastForwardingCache(void) const;
-    static void        HandleBackboneMulticastListenerEvent(void *                                 aContext,
+    static void        HandleBackboneMulticastListenerEvent(void                                  *aContext,
                                                             otBackboneRouterMulticastListenerEvent aEvent,
-                                                            const otIp6Address *                   aAddress);
+                                                            const otIp6Address                    *aAddress);
     void               HandleBackboneMulticastListenerEvent(otBackboneRouterMulticastListenerEvent aEvent,
-                                                            const Ip6::Address &                   aAddress);
+                                                            const Ip6::Address                    &aAddress);
 
-    MulticastForwardingCache mMulticastForwardingCacheTable[kMulitcastForwardingCacheTableSize];
+    MulticastForwardingCache mMulticastForwardingCacheTable[kMulticastForwardingCacheTableSize];
     uint64_t                 mLastExpireTime;
     int                      mMulticastRouterSock;
 };
diff --git a/src/posix/platform/netif.cpp b/src/posix/platform/netif.cpp
index 2d761ce..1f238cf 100644
--- a/src/posix/platform/netif.cpp
+++ b/src/posix/platform/netif.cpp
@@ -72,6 +72,7 @@
 #include <fcntl.h>
 #include <ifaddrs.h>
 #ifdef __linux__
+#include <linux/if_link.h>
 #include <linux/if_tun.h>
 #include <linux/netlink.h>
 #include <linux/rtnetlink.h>
@@ -143,6 +144,7 @@
 #include <openthread/ip6.h>
 #include <openthread/logging.h>
 #include <openthread/message.h>
+#include <openthread/nat64.h>
 #include <openthread/netdata.h>
 #include <openthread/platform/misc.h>
 
@@ -150,18 +152,15 @@
 #include "common/debug.hpp"
 #include "net/ip6_address.hpp"
 
+#include "resolver.hpp"
+
 unsigned int gNetifIndex = 0;
 char         gNetifName[IFNAMSIZ];
+otIp4Cidr    gNat64Cidr;
 
-const char *otSysGetThreadNetifName(void)
-{
-    return gNetifName;
-}
+const char *otSysGetThreadNetifName(void) { return gNetifName; }
 
-unsigned int otSysGetThreadNetifIndex(void)
-{
-    return gNetifIndex;
-}
+unsigned int otSysGetThreadNetifIndex(void) { return gNetifIndex; }
 
 #if OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE
 #if OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE
@@ -208,6 +207,14 @@
 static otIp6Prefix        sAddedExternalRoutes[kMaxExternalRoutesNum];
 #endif
 
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+static constexpr uint32_t kNat64RoutePriority = 100; ///< Priority for route to NAT64 CIDR, 100 means a high priority.
+#endif
+
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+ot::Posix::Resolver gResolver;
+#endif
+
 #if defined(RTM_NEWMADDR) || defined(__NetBSD__)
 // on some BSDs (mac OS, FreeBSD), we get RTM_NEWMADDR/RTM_DELMADDR messages, so we don't need to monitor using MLD
 // on NetBSD, MLD monitoring simply doesn't work
@@ -330,7 +337,11 @@
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wcast-align"
 
-void AddRtAttr(struct nlmsghdr *aHeader, uint32_t aMaxLen, uint8_t aType, const void *aData, uint8_t aLen)
+static struct rtattr *AddRtAttr(struct nlmsghdr *aHeader,
+                                uint32_t         aMaxLen,
+                                uint8_t          aType,
+                                const void      *aData,
+                                uint8_t          aLen)
 {
     uint8_t        len = RTA_LENGTH(aLen);
     struct rtattr *rta;
@@ -346,6 +357,8 @@
         memcpy(RTA_DATA(rta), aData, aLen);
     }
     aHeader->nlmsg_len = NLMSG_ALIGN(aHeader->nlmsg_len) + RTA_ALIGN(len);
+
+    return rta;
 }
 
 void AddRtAttrUint32(struct nlmsghdr *aHeader, uint32_t aMaxLen, uint8_t aType, uint32_t aData)
@@ -527,12 +540,13 @@
     SuccessOrDie(error);
 }
 
-static void UpdateLink(otInstance *aInstance)
+static void SetLinkState(otInstance *aInstance, bool aState)
 {
+    OT_UNUSED_VARIABLE(aInstance);
+
     otError      error = OT_ERROR_NONE;
     struct ifreq ifr;
     bool         ifState = false;
-    bool         otState = false;
 
     assert(gInstance == aInstance);
 
@@ -542,14 +556,13 @@
     VerifyOrExit(ioctl(sIpFd, SIOCGIFFLAGS, &ifr) == 0, perror("ioctl"); error = OT_ERROR_FAILED);
 
     ifState = ((ifr.ifr_flags & IFF_UP) == IFF_UP) ? true : false;
-    otState = otIp6IsEnabled(aInstance);
 
-    otLogNotePlat("[netif] Changing interface state to %s%s.", otState ? "up" : "down",
-                  (ifState == otState) ? " (already done, ignoring)" : "");
+    otLogNotePlat("[netif] Changing interface state to %s%s.", aState ? "up" : "down",
+                  (ifState == aState) ? " (already done, ignoring)" : "");
 
-    if (ifState != otState)
+    if (ifState != aState)
     {
-        ifr.ifr_flags = otState ? (ifr.ifr_flags | IFF_UP) : (ifr.ifr_flags & ~IFF_UP);
+        ifr.ifr_flags = aState ? (ifr.ifr_flags | IFF_UP) : (ifr.ifr_flags & ~IFF_UP);
         VerifyOrExit(ioctl(sIpFd, SIOCSIFFLAGS, &ifr) == 0, perror("ioctl"); error = OT_ERROR_FAILED);
 #if defined(RTM_NEWLINK) && defined(RTM_DELLINK)
         // wait for RTM_NEWLINK event before processing notification from kernel to avoid infinite loop
@@ -564,10 +577,14 @@
     }
 }
 
-#if __linux__ && \
-    (OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE || OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE)
+static void UpdateLink(otInstance *aInstance)
+{
+    assert(gInstance == aInstance);
+    SetLinkState(aInstance, otIp6IsEnabled(aInstance));
+}
 
-static otError AddRoute(const otIp6Prefix &aPrefix, uint32_t aPriority)
+#if defined(__linux__)
+template <size_t N> otError AddRoute(const uint8_t (&aAddress)[N], uint8_t aPrefixLen, uint32_t aPriority)
 {
     constexpr unsigned int kBufSize = 128;
     struct
@@ -576,10 +593,10 @@
         struct rtmsg    msg;
         char            buf[kBufSize];
     } req{};
-    unsigned char data[sizeof(in6_addr)];
-    char          addrBuf[OT_IP6_ADDRESS_STRING_SIZE];
-    unsigned int  netifIdx = otSysGetThreadNetifIndex();
-    otError       error    = OT_ERROR_NONE;
+    unsigned int netifIdx = otSysGetThreadNetifIndex();
+    otError      error    = OT_ERROR_NONE;
+
+    static_assert(N == sizeof(in6_addr) || N == sizeof(in_addr), "aAddress should be 4 octets or 16 octets");
 
     VerifyOrExit(netifIdx > 0, error = OT_ERROR_INVALID_STATE);
     VerifyOrExit(sNetlinkFd >= 0, error = OT_ERROR_INVALID_STATE);
@@ -591,9 +608,9 @@
     req.header.nlmsg_pid  = 0;
     req.header.nlmsg_seq  = ++sNetlinkSequence;
 
-    req.msg.rtm_family   = AF_INET6;
+    req.msg.rtm_family   = (N == sizeof(in6_addr) ? AF_INET6 : AF_INET);
     req.msg.rtm_src_len  = 0;
-    req.msg.rtm_dst_len  = aPrefix.mLength;
+    req.msg.rtm_dst_len  = aPrefixLen;
     req.msg.rtm_tos      = 0;
     req.msg.rtm_scope    = RT_SCOPE_UNIVERSE;
     req.msg.rtm_type     = RTN_UNICAST;
@@ -601,9 +618,7 @@
     req.msg.rtm_protocol = RTPROT_BOOT;
     req.msg.rtm_flags    = 0;
 
-    otIp6AddressToString(&aPrefix.mPrefix, addrBuf, OT_IP6_ADDRESS_STRING_SIZE);
-    inet_pton(AF_INET6, addrBuf, data);
-    AddRtAttr(reinterpret_cast<nlmsghdr *>(&req), sizeof(req), RTA_DST, data, sizeof(data));
+    AddRtAttr(reinterpret_cast<nlmsghdr *>(&req), sizeof(req), RTA_DST, aAddress, sizeof(aAddress));
     AddRtAttrUint32(&req.header, sizeof(req), RTA_PRIORITY, aPriority);
     AddRtAttrUint32(&req.header, sizeof(req), RTA_OIF, netifIdx);
 
@@ -616,7 +631,7 @@
     return error;
 }
 
-static otError DeleteRoute(const otIp6Prefix &aPrefix)
+template <size_t N> otError DeleteRoute(const uint8_t (&aAddress)[N], uint8_t aPrefixLen)
 {
     constexpr unsigned int kBufSize = 512;
     struct
@@ -625,10 +640,10 @@
         struct rtmsg    msg;
         char            buf[kBufSize];
     } req{};
-    unsigned char data[sizeof(in6_addr)];
-    char          addrBuf[OT_IP6_ADDRESS_STRING_SIZE];
-    unsigned int  netifIdx = otSysGetThreadNetifIndex();
-    otError       error    = OT_ERROR_NONE;
+    unsigned int netifIdx = otSysGetThreadNetifIndex();
+    otError      error    = OT_ERROR_NONE;
+
+    static_assert(N == sizeof(in6_addr) || N == sizeof(in_addr), "aAddress should be 4 octets or 16 octets");
 
     VerifyOrExit(netifIdx > 0, error = OT_ERROR_INVALID_STATE);
     VerifyOrExit(sNetlinkFd >= 0, error = OT_ERROR_INVALID_STATE);
@@ -640,9 +655,9 @@
     req.header.nlmsg_pid  = 0;
     req.header.nlmsg_seq  = ++sNetlinkSequence;
 
-    req.msg.rtm_family   = AF_INET6;
+    req.msg.rtm_family   = (N == sizeof(in6_addr) ? AF_INET6 : AF_INET);
     req.msg.rtm_src_len  = 0;
-    req.msg.rtm_dst_len  = aPrefix.mLength;
+    req.msg.rtm_dst_len  = aPrefixLen;
     req.msg.rtm_tos      = 0;
     req.msg.rtm_scope    = RT_SCOPE_UNIVERSE;
     req.msg.rtm_type     = RTN_UNICAST;
@@ -650,9 +665,7 @@
     req.msg.rtm_protocol = RTPROT_BOOT;
     req.msg.rtm_flags    = 0;
 
-    otIp6AddressToString(&aPrefix.mPrefix, addrBuf, OT_IP6_ADDRESS_STRING_SIZE);
-    inet_pton(AF_INET6, addrBuf, data);
-    AddRtAttr(reinterpret_cast<nlmsghdr *>(&req), sizeof(req), RTA_DST, data, sizeof(data));
+    AddRtAttr(reinterpret_cast<nlmsghdr *>(&req), sizeof(req), RTA_DST, &aAddress, sizeof(aAddress));
     AddRtAttrUint32(&req.header, sizeof(req), RTA_OIF, netifIdx);
 
     if (send(sNetlinkFd, &req, sizeof(req), 0) < 0)
@@ -665,11 +678,19 @@
     return error;
 }
 
-#endif // __linux__ && (OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE ||
-       // OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE)
+#if OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE || OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE
+static otError AddRoute(const otIp6Prefix &aPrefix, uint32_t aPriority)
+{
+    return AddRoute(aPrefix.mPrefix.mFields.m8, aPrefix.mLength, aPriority);
+}
 
-#if OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE && __linux__
+static otError DeleteRoute(const otIp6Prefix &aPrefix)
+{
+    return DeleteRoute(aPrefix.mPrefix.mFields.m8, aPrefix.mLength);
+}
+#endif // OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE || OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE
 
+#if OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE
 static bool HasAddedOmrRoute(const otIp6Prefix &aOmrPrefix)
 {
     bool found = false;
@@ -744,15 +765,13 @@
         else
         {
             sAddedOmrRoutes[sAddedOmrRoutesNum++] = config.mPrefix;
-            otLogInfoPlat("[netif] Successfully added an OMR route %s in kernel: %s", prefixString);
+            otLogInfoPlat("[netif] Successfully added an OMR route %s in kernel", prefixString);
         }
     }
 }
+#endif // OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE
 
-#endif // OPENTHREAD_POSIX_CONFIG_INSTALL_OMR_ROUTES_ENABLE && __linux__
-
-#if OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE && __linux__
-
+#if OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE
 static otError AddExternalRoute(const otIp6Prefix &aPrefix)
 {
     otError error;
@@ -843,13 +862,26 @@
         else
         {
             sAddedExternalRoutes[sAddedExternalRoutesNum++] = config.mPrefix;
-            otLogWarnPlat("[netif] Successfully added an external route %s in kernel: %s", prefixString);
+            otLogWarnPlat("[netif] Successfully added an external route %s in kernel", prefixString);
         }
     }
 exit:
     return;
 }
-#endif // OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE && __linux__
+#endif // OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE
+
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+static otError AddIp4Route(const otIp4Cidr &aIp4Cidr, uint32_t aPriority)
+{
+    return AddRoute(aIp4Cidr.mAddress.mFields.m8, aIp4Cidr.mLength, aPriority);
+}
+
+static otError DeleteIp4Route(const otIp4Cidr &aIp4Cidr)
+{
+    return DeleteRoute(aIp4Cidr.mAddress.mFields.m8, aIp4Cidr.mLength);
+}
+#endif
+#endif // defined(__linux__)
 
 static void processAddressChange(const otIp6AddressInfo *aAddressInfo, bool aIsAdded, void *aContext)
 {
@@ -863,6 +895,28 @@
     }
 }
 
+#if defined(__linux__) && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+void processNat64StateChange(otNat64State aNewState)
+{
+    // If the interface is not up and we are enabling NAT64, the route will be added when we bring up the route.
+    // Also, the route will be deleted by the kernel when we shutting down the interface.
+    // We should try to add route first, since otNat64SetEnabled never fails.
+    if (otIp6IsEnabled(gInstance))
+    {
+        if (aNewState == OT_NAT64_STATE_ACTIVE)
+        {
+            AddIp4Route(gNat64Cidr, kNat64RoutePriority);
+            otLogInfoPlat("[netif] Adding route for NAT64");
+        }
+        else
+        {
+            DeleteIp4Route(gNat64Cidr);
+            otLogInfoPlat("[netif] Deleting route for NAT64");
+        }
+    }
+}
+#endif // defined(__linux__) && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+
 void platformNetifStateChange(otInstance *aInstance, otChangedFlags aFlags)
 {
     if (OT_CHANGED_THREAD_NETIF_STATE & aFlags)
@@ -881,6 +935,12 @@
         ot::Posix::UpdateIpSets(aInstance);
 #endif
     }
+#if defined(__linux__) && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    if (OT_CHANGED_NAT64_TRANSLATOR_STATE & aFlags)
+    {
+        processNat64StateChange(otNat64GetTranslatorState(aInstance));
+    }
+#endif
 }
 
 static void processReceive(otMessage *aMessage, void *aContext)
@@ -935,21 +995,15 @@
     char       packet[kMaxIp6Size];
     otError    error  = OT_ERROR_NONE;
     size_t     offset = 0;
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    bool isIp4 = false;
+#endif
 
     assert(gInstance == aInstance);
 
     rval = read(sTunFd, packet, sizeof(packet));
     VerifyOrExit(rval > 0, error = OT_ERROR_FAILED);
 
-    {
-        otMessageSettings settings;
-
-        settings.mLinkSecurityEnabled = (otThreadGetDeviceRole(aInstance) != OT_DEVICE_ROLE_DISABLED);
-        settings.mPriority            = OT_MESSAGE_PRIORITY_LOW;
-        message                       = otIp6NewMessage(aInstance, &settings);
-        VerifyOrExit(message != nullptr, error = OT_ERROR_NO_BUFS);
-    }
-
 #if defined(__APPLE__) || defined(__NetBSD__) || defined(__FreeBSD__)
     // BSD tunnel drivers have (for legacy reasons), may have a 4-byte header on them
     if ((rval >= 4) && (packet[0] == 0) && (packet[1] == 0))
@@ -959,6 +1013,20 @@
     }
 #endif
 
+    {
+        otMessageSettings settings;
+
+        settings.mLinkSecurityEnabled = (otThreadGetDeviceRole(aInstance) != OT_DEVICE_ROLE_DISABLED);
+        settings.mPriority            = OT_MESSAGE_PRIORITY_LOW;
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+        isIp4   = (packet[offset] & 0xf0) == 0x40;
+        message = isIp4 ? otIp4NewMessage(aInstance, &settings) : otIp6NewMessage(aInstance, &settings);
+#else
+        message = otIp6NewMessage(aInstance, &settings);
+#endif
+        VerifyOrExit(message != nullptr, error = OT_ERROR_NO_BUFS);
+    }
+
 #if OPENTHREAD_POSIX_LOG_TUN_PACKETS
     otLogInfoPlat("[netif] Packet to NCP (%hu bytes)", static_cast<uint16_t>(rval));
     otDumpInfoPlat("", &packet[offset], static_cast<size_t>(rval));
@@ -966,7 +1034,11 @@
 
     SuccessOrExit(error = otMessageAppend(message, &packet[offset], static_cast<uint16_t>(rval)));
 
-    error   = otIp6Send(aInstance, message);
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    error = isIp4 ? otNat64Send(aInstance, message) : otIp6Send(aInstance, message);
+#else
+    error = otIp6Send(aInstance, message);
+#endif
     message = nullptr;
 
 exit:
@@ -979,7 +1051,7 @@
     {
         if (error == OT_ERROR_DROP)
         {
-            otLogInfoPlat("[netif] Message dropped by Thread", otThreadErrorToString(error));
+            otLogInfoPlat("[netif] Message dropped by Thread");
         }
         else
         {
@@ -997,10 +1069,10 @@
     {
         otLogInfoPlat("[netif] %s [%s] %s%s", isAdd ? "ADD" : "DEL", aAddress.IsMulticast() ? "M" : "U",
                       aAddress.ToString().AsCString(),
-                      error == OT_ERROR_ALREADY
-                          ? " (already subscribed, ignored)"
-                          : error == OT_ERROR_REJECTED ? " (rejected)"
-                                                       : error == OT_ERROR_NOT_FOUND ? " (not found, ignored)" : "");
+                      error == OT_ERROR_ALREADY     ? " (already subscribed, ignored)"
+                      : error == OT_ERROR_REJECTED  ? " (rejected)"
+                      : error == OT_ERROR_NOT_FOUND ? " (not found, ignored)"
+                                                    : "");
     }
     else
     {
@@ -1013,7 +1085,7 @@
 
 static void processNetifAddrEvent(otInstance *aInstance, struct nlmsghdr *aNetlinkMessage)
 {
-    struct ifaddrmsg *  ifaddr = reinterpret_cast<struct ifaddrmsg *>(NLMSG_DATA(aNetlinkMessage));
+    struct ifaddrmsg   *ifaddr = reinterpret_cast<struct ifaddrmsg *>(NLMSG_DATA(aNetlinkMessage));
     size_t              rtaLength;
     otError             error = OT_ERROR_NONE;
     struct sockaddr_in6 addr6;
@@ -1134,13 +1206,21 @@
         otLogInfoPlat("[netif] Succeeded to sync netif state with host");
     }
 
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE && OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    if (isUp && gNat64Cidr.mLength > 0)
+    {
+        SuccessOrExit(error = otNat64SetIp4Cidr(gInstance, &gNat64Cidr));
+        otLogInfoPlat("[netif] Succeeded to enable NAT64");
+    }
+#endif
+
 exit:
     if (error != OT_ERROR_NONE)
     {
         otLogWarnPlat("[netif] Failed to sync netif state with host: %s", otThreadErrorToString(error));
     }
 }
-#endif
+#endif // defined(__linux__)
 
 #if defined(__APPLE__) || defined(__NetBSD__) || defined(__FreeBSD__)
 
@@ -1169,10 +1249,10 @@
 #endif
     struct sockaddr_in6 addr6;
     struct sockaddr_in6 netmask;
-    uint8_t *           addrbuf;
+    uint8_t            *addrbuf;
     unsigned int        addrmask = 0;
     unsigned int        i;
-    struct sockaddr *   sa;
+    struct sockaddr    *sa;
     bool                is_link_local;
 
     addr6.sin6_family   = 0;
@@ -1493,10 +1573,10 @@
     struct sockaddr_in6 srcAddr;
     socklen_t           addrLen  = sizeof(srcAddr);
     bool                fromSelf = false;
-    MLDv2Header *       hdr      = reinterpret_cast<MLDv2Header *>(buffer);
+    MLDv2Header        *hdr      = reinterpret_cast<MLDv2Header *>(buffer);
     size_t              offset;
     uint8_t             type;
-    struct ifaddrs *    ifAddrs = nullptr;
+    struct ifaddrs     *ifAddrs = nullptr;
     char                addressString[INET6_ADDRSTRLEN + 1];
 
     bufferLen = recvfrom(sMLDMonitorFd, buffer, sizeof(buffer), 0, reinterpret_cast<sockaddr *>(&srcAddr), &addrLen);
@@ -1564,22 +1644,71 @@
 #endif
 
 #if defined(__linux__)
+static void SetAddrGenModeToNone(void)
+{
+    struct
+    {
+        struct nlmsghdr  nh;
+        struct ifinfomsg ifi;
+        char             buf[512];
+    } req;
+
+    const enum in6_addr_gen_mode mode = IN6_ADDR_GEN_MODE_NONE;
+
+    memset(&req, 0, sizeof(req));
+
+    req.nh.nlmsg_len   = NLMSG_LENGTH(sizeof(struct ifinfomsg));
+    req.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+    req.nh.nlmsg_type  = RTM_NEWLINK;
+    req.nh.nlmsg_pid   = 0;
+    req.nh.nlmsg_seq   = ++sNetlinkSequence;
+
+    req.ifi.ifi_index  = static_cast<int>(gNetifIndex);
+    req.ifi.ifi_change = 0xffffffff;
+    req.ifi.ifi_flags  = IFF_MULTICAST | IFF_NOARP;
+
+    {
+        struct rtattr *afSpec  = AddRtAttr(&req.nh, sizeof(req), IFLA_AF_SPEC, 0, 0);
+        struct rtattr *afInet6 = AddRtAttr(&req.nh, sizeof(req), AF_INET6, 0, 0);
+        struct rtattr *inet6AddrGenMode =
+            AddRtAttr(&req.nh, sizeof(req), IFLA_INET6_ADDR_GEN_MODE, &mode, sizeof(mode));
+
+        afInet6->rta_len += inet6AddrGenMode->rta_len;
+        afSpec->rta_len += afInet6->rta_len;
+    }
+
+    if (send(sNetlinkFd, &req, req.nh.nlmsg_len, 0) != -1)
+    {
+        otLogInfoPlat("[netif] Sent request#%u to set addr_gen_mode to %d", sNetlinkSequence, mode);
+    }
+    else
+    {
+        otLogWarnPlat("[netif] Failed to send request#%u to set addr_gen_mode to %d", sNetlinkSequence, mode);
+    }
+}
+
 // set up the tun device
-static void platformConfigureTunDevice(const char *aInterfaceName, char *deviceName, size_t deviceNameLen)
+static void platformConfigureTunDevice(otPlatformConfig *aPlatformConfig)
 {
     struct ifreq ifr;
+    const char  *interfaceName;
 
     sTunFd = open(OPENTHREAD_POSIX_TUN_DEVICE, O_RDWR | O_CLOEXEC | O_NONBLOCK);
     VerifyOrDie(sTunFd >= 0, OT_EXIT_ERROR_ERRNO);
 
     memset(&ifr, 0, sizeof(ifr));
-    ifr.ifr_flags = IFF_TUN | IFF_NO_PI | static_cast<short>(IFF_TUN_EXCL);
-
-    if (aInterfaceName)
+    ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
+    if (!aPlatformConfig->mPersistentInterface)
     {
-        VerifyOrDie(strlen(aInterfaceName) < IFNAMSIZ, OT_EXIT_INVALID_ARGUMENTS);
+        ifr.ifr_flags |= static_cast<short>(IFF_TUN_EXCL);
+    }
 
-        strncpy(ifr.ifr_name, aInterfaceName, IFNAMSIZ);
+    interfaceName = aPlatformConfig->mInterfaceName;
+    if (interfaceName)
+    {
+        VerifyOrDie(strlen(interfaceName) < IFNAMSIZ, OT_EXIT_INVALID_ARGUMENTS);
+
+        strncpy(ifr.ifr_name, interfaceName, IFNAMSIZ);
     }
     else
     {
@@ -1587,9 +1716,18 @@
     }
 
     VerifyOrDie(ioctl(sTunFd, TUNSETIFF, static_cast<void *>(&ifr)) == 0, OT_EXIT_ERROR_ERRNO);
-    VerifyOrDie(ioctl(sTunFd, TUNSETLINK, ARPHRD_VOID) == 0, OT_EXIT_ERROR_ERRNO);
 
-    strncpy(deviceName, ifr.ifr_name, deviceNameLen);
+    strncpy(gNetifName, ifr.ifr_name, sizeof(gNetifName));
+
+    if (aPlatformConfig->mPersistentInterface)
+    {
+        VerifyOrDie(ioctl(sTunFd, TUNSETPERSIST, 1) == 0, OT_EXIT_ERROR_ERRNO);
+        // Set link down to reset the tun configuration.
+        // This will drop all existing IP addresses on the interface.
+        SetLinkState(gInstance, false);
+    }
+
+    VerifyOrDie(ioctl(sTunFd, TUNSETLINK, ARPHRD_NONE) == 0, OT_EXIT_ERROR_ERRNO);
 
     ifr.ifr_mtu = static_cast<int>(kMaxIp6Size);
     VerifyOrDie(ioctl(sIpFd, SIOCSIFMTU, static_cast<void *>(&ifr)) == 0, OT_EXIT_ERROR_ERRNO);
@@ -1597,9 +1735,9 @@
 #endif
 
 #if defined(__APPLE__) && (OPENTHREAD_POSIX_CONFIG_MACOS_TUN_OPTION == OT_POSIX_CONFIG_MACOS_UTUN)
-static void platformConfigureTunDevice(const char *aInterfaceName, char *deviceName, size_t deviceNameLen)
+static void platformConfigureTunDevice(otPlatformConfig *aPlatformConfig)
 {
-    (void)aInterfaceName;
+    (void)aPlatformConfig;
     int                 err = 0;
     struct sockaddr_ctl addr;
     struct ctl_info     info;
@@ -1622,11 +1760,11 @@
     VerifyOrDie(err == 0, OT_EXIT_ERROR_ERRNO);
 
     socklen_t devNameLen;
-    devNameLen = (socklen_t)deviceNameLen;
-    err        = getsockopt(sTunFd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, deviceName, &devNameLen);
+    devNameLen = (socklen_t)sizeof(gNetifName);
+    err        = getsockopt(sTunFd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, gNetifName, &devNameLen);
     VerifyOrDie(err == 0, OT_EXIT_ERROR_ERRNO);
 
-    otLogInfoPlat("[netif] Tunnel device name = '%s'", deviceName);
+    otLogInfoPlat("[netif] Tunnel device name = '%s'", gNetifName);
 }
 #endif
 
@@ -1649,14 +1787,14 @@
 #if defined(__NetBSD__) ||                                                                             \
     (defined(__APPLE__) && (OPENTHREAD_POSIX_CONFIG_MACOS_TUN_OPTION == OT_POSIX_CONFIG_MACOS_TUN)) || \
     defined(__FreeBSD__)
-static void platformConfigureTunDevice(const char *aInterfaceName, char *deviceName, size_t deviceNameLen)
+static void platformConfigureTunDevice(otPlatformConfig *aPlatformConfig)
 {
     int         flags = IFF_BROADCAST | IFF_MULTICAST;
     int         err;
     const char *last_slash;
     const char *path;
 
-    (void)aInterfaceName;
+    (void)aPlatformConfig;
 
     path = OPENTHREAD_POSIX_TUN_DEVICE;
 
@@ -1676,7 +1814,7 @@
     VerifyOrDie(last_slash != nullptr, OT_EXIT_ERROR_ERRNO);
     last_slash++;
 
-    strncpy(deviceName, last_slash, deviceNameLen);
+    strncpy(gNetifName, last_slash, sizeof(gNetifName));
 }
 #endif
 
@@ -1728,13 +1866,13 @@
 #endif // defined(__APPLE__) || defined(__NetBSD__) || defined(__FreeBSD__)
 }
 
-void platformNetifInit(const char *aInterfaceName)
+void platformNetifInit(otPlatformConfig *aPlatformConfig)
 {
     sIpFd = SocketWithCloseExec(AF_INET6, SOCK_DGRAM, IPPROTO_IP, kSocketNonBlock);
     VerifyOrDie(sIpFd >= 0, OT_EXIT_ERROR_ERRNO);
 
     platformConfigureNetLink();
-    platformConfigureTunDevice(aInterfaceName, gNetifName, sizeof(gNetifName));
+    platformConfigureTunDevice(aPlatformConfig);
 
     gNetifIndex = if_nametoindex(gNetifName);
     VerifyOrDie(gNetifIndex > 0, OT_EXIT_FAILURE);
@@ -1742,6 +1880,10 @@
 #if OPENTHREAD_POSIX_USE_MLD_MONITOR
     mldListenerInit();
 #endif
+
+#if __linux__
+    SetAddrGenModeToNone();
+#endif
 }
 
 void platformNetifSetUp(void)
@@ -1755,15 +1897,20 @@
     otIcmp6SetEchoMode(gInstance, OT_ICMP6_ECHO_HANDLER_DISABLED);
 #endif
     otIp6SetReceiveCallback(gInstance, processReceive, gInstance);
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    // We can use the same function for IPv6 and translated IPv4 messages.
+    otNat64SetReceiveIp4Callback(gInstance, processReceive, gInstance);
+#endif
     otIp6SetAddressCallback(gInstance, processAddressChange, gInstance);
 #if OPENTHREAD_POSIX_MULTICAST_PROMISCUOUS_REQUIRED
     otIp6SetMulticastPromiscuousEnabled(aInstance, true);
 #endif
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    gResolver.Init();
+#endif
 }
 
-void platformNetifTearDown(void)
-{
-}
+void platformNetifTearDown(void) {}
 
 void platformNetifDeinit(void)
 {
@@ -1800,87 +1947,94 @@
     gNetifIndex = 0;
 }
 
-void platformNetifUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, fd_set *aErrorFdSet, int *aMaxFd)
+void platformNetifUpdateFdSet(otSysMainloopContext *aContext)
 {
-    OT_UNUSED_VARIABLE(aWriteFdSet);
-
     VerifyOrExit(gNetifIndex > 0);
 
+    assert(aContext != nullptr);
     assert(sTunFd >= 0);
     assert(sNetlinkFd >= 0);
     assert(sIpFd >= 0);
 
-    FD_SET(sTunFd, aReadFdSet);
-    FD_SET(sTunFd, aErrorFdSet);
-    FD_SET(sNetlinkFd, aReadFdSet);
-    FD_SET(sNetlinkFd, aErrorFdSet);
+    FD_SET(sTunFd, &aContext->mReadFdSet);
+    FD_SET(sTunFd, &aContext->mErrorFdSet);
+    FD_SET(sNetlinkFd, &aContext->mReadFdSet);
+    FD_SET(sNetlinkFd, &aContext->mErrorFdSet);
 #if OPENTHREAD_POSIX_USE_MLD_MONITOR
-    FD_SET(sMLDMonitorFd, aReadFdSet);
-    FD_SET(sMLDMonitorFd, aErrorFdSet);
+    FD_SET(sMLDMonitorFd, &aContext->mReadFdSet);
+    FD_SET(sMLDMonitorFd, &aContext->mErrorFdSet);
 #endif
 
-    if (sTunFd > *aMaxFd)
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    gResolver.UpdateFdSet(*aContext);
+#endif
+
+    if (sTunFd > aContext->mMaxFd)
     {
-        *aMaxFd = sTunFd;
+        aContext->mMaxFd = sTunFd;
     }
 
-    if (sNetlinkFd > *aMaxFd)
+    if (sNetlinkFd > aContext->mMaxFd)
     {
-        *aMaxFd = sNetlinkFd;
+        aContext->mMaxFd = sNetlinkFd;
     }
 
 #if OPENTHREAD_POSIX_USE_MLD_MONITOR
-    if (sMLDMonitorFd > *aMaxFd)
+    if (sMLDMonitorFd > aContext->mMaxFd)
     {
-        *aMaxFd = sMLDMonitorFd;
+        aContext->mMaxFd = sMLDMonitorFd;
     }
 #endif
 exit:
     return;
 }
 
-void platformNetifProcess(const fd_set *aReadFdSet, const fd_set *aWriteFdSet, const fd_set *aErrorFdSet)
+void platformNetifProcess(const otSysMainloopContext *aContext)
 {
-    OT_UNUSED_VARIABLE(aWriteFdSet);
+    assert(aContext != nullptr);
     VerifyOrExit(gNetifIndex > 0);
 
-    if (FD_ISSET(sTunFd, aErrorFdSet))
+    if (FD_ISSET(sTunFd, &aContext->mErrorFdSet))
     {
         close(sTunFd);
         DieNow(OT_EXIT_FAILURE);
     }
 
-    if (FD_ISSET(sNetlinkFd, aErrorFdSet))
+    if (FD_ISSET(sNetlinkFd, &aContext->mErrorFdSet))
     {
         close(sNetlinkFd);
         DieNow(OT_EXIT_FAILURE);
     }
 
 #if OPENTHREAD_POSIX_USE_MLD_MONITOR
-    if (FD_ISSET(sMLDMonitorFd, aErrorFdSet))
+    if (FD_ISSET(sMLDMonitorFd, &aContext->mErrorFdSet))
     {
         close(sMLDMonitorFd);
         DieNow(OT_EXIT_FAILURE);
     }
 #endif
 
-    if (FD_ISSET(sTunFd, aReadFdSet))
+    if (FD_ISSET(sTunFd, &aContext->mReadFdSet))
     {
         processTransmit(gInstance);
     }
 
-    if (FD_ISSET(sNetlinkFd, aReadFdSet))
+    if (FD_ISSET(sNetlinkFd, &aContext->mReadFdSet))
     {
         processNetlinkEvent(gInstance);
     }
 
 #if OPENTHREAD_POSIX_USE_MLD_MONITOR
-    if (FD_ISSET(sMLDMonitorFd, aReadFdSet))
+    if (FD_ISSET(sMLDMonitorFd, &aContext->mReadFdSet))
     {
         processMLDEvent(gInstance);
     }
 #endif
 
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    gResolver.Process(*aContext);
+#endif
+
 exit:
     return;
 }
diff --git a/src/posix/platform/openthread-core-posix-config.h b/src/posix/platform/openthread-core-posix-config.h
index 6708751..0a3cd20 100644
--- a/src/posix/platform/openthread-core-posix-config.h
+++ b/src/posix/platform/openthread-core-posix-config.h
@@ -45,6 +45,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_DIAG_OUTPUT_BUFFER_SIZE
+ *
+ * Define OpenThread diagnostic mode output buffer size in bytes
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_DIAG_OUTPUT_BUFFER_SIZE
+#define OPENTHREAD_CONFIG_DIAG_OUTPUT_BUFFER_SIZE 500
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_LOG_PLATFORM
  *
  * Define to enable platform region logging.
@@ -299,4 +309,24 @@
 #define OPENTHREAD_CONFIG_SRP_CLIENT_BUFFERS_MAX_SERVICES 20
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_ASSERT_CHECK_API_POINTER_PARAM_FOR_NULL
+ *
+ * Define as 1 to enable assert check of pointer-type API input parameters against null.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_ASSERT_CHECK_API_POINTER_PARAM_FOR_NULL
+#define OPENTHREAD_CONFIG_ASSERT_CHECK_API_POINTER_PARAM_FOR_NULL 1
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+ *
+ * Define as 1 to enable platform power calibration support.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+#define OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE 1
+#endif
+
 #endif // OPENTHREAD_CORE_POSIX_CONFIG_H_
diff --git a/src/posix/platform/openthread-posix-config.h b/src/posix/platform/openthread-posix-config.h
index 191f5a0..b7f4cd5 100644
--- a/src/posix/platform/openthread-posix-config.h
+++ b/src/posix/platform/openthread-posix-config.h
@@ -31,6 +31,10 @@
 
 #include "openthread-core-config.h"
 
+#ifdef OPENTHREAD_POSIX_CONFIG_FILE
+#include OPENTHREAD_POSIX_CONFIG_FILE
+#endif
+
 /**
  * @file
  * @brief
@@ -72,6 +76,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_POSIX_CONFIG_DAEMON_CLI_ENABLE
+ *
+ * Define to 1 to enable CLI for the posix daemon.
+ *
+ */
+#ifndef OPENTHREAD_POSIX_CONFIG_DAEMON_CLI_ENABLE
+#define OPENTHREAD_POSIX_CONFIG_DAEMON_CLI_ENABLE 1
+#endif
+
+/**
  * RCP bus UART.
  *
  * @note This value is also for simulated UART bus.
@@ -215,6 +229,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_POSIX_CONFIG_NAT64_AIL_PREFIX_ENABLE
+ *
+ * Define as 1 to enable discovering NAT64 posix on adjacent infrastructure link.
+ *
+ */
+#ifndef OPENTHREAD_POSIX_CONFIG_NAT64_AIL_PREFIX_ENABLE
+#define OPENTHREAD_POSIX_CONFIG_NAT64_AIL_PREFIX_ENABLE 0
+#endif
+
+/**
  * @def OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE
  *
  * Define as 1 to enable firewall.
@@ -238,6 +262,16 @@
 #endif
 #endif
 
+/**
+ * @def OPENTHREAD_POSIX_CONFIG_THREAD_NETIF_DEFAULT_NAME
+ *
+ * Define the Thread default network interface name.
+ *
+ */
+#ifndef OPENTHREAD_POSIX_CONFIG_THREAD_NETIF_DEFAULT_NAME
+#define OPENTHREAD_POSIX_CONFIG_THREAD_NETIF_DEFAULT_NAME "wpan0"
+#endif
+
 #ifdef __APPLE__
 
 /**
@@ -284,4 +318,79 @@
 #define OPENTHREAD_POSIX_CONFIG_TREL_UDP_PORT 0
 #endif
 
+/**
+ * @def OPENTHREAD_POSIX_CONFIG_NAT64_CIDR
+ *
+ * This setting configures the NAT64 CIDR, used by NAT64 translator.
+ *
+ */
+#ifndef OPENTHREAD_POSIX_CONFIG_NAT64_CIDR
+#define OPENTHREAD_POSIX_CONFIG_NAT64_CIDR "192.168.255.0/24"
+#endif
+
+/**
+ * @def OPENTHREAD_POSIX_CONFIG_BACKTRACE_ENABLE
+ *
+ * Define as 1 to enable backtrace support.
+ *
+ */
+#ifndef OPENTHREAD_POSIX_CONFIG_BACKTRACE_ENABLE
+#define OPENTHREAD_POSIX_CONFIG_BACKTRACE_ENABLE 1
+#endif
+
+/**
+ * @def OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE
+ *
+ * Define as 1 to enable android support.
+ *
+ */
+#ifndef OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE
+#define OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE 0
+#endif
+
+/**
+ * @def OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
+ *
+ * Defines `1` to enable the posix implementation of platform/infra_if.h APIs.
+ * The default value is set to `OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE` if it's
+ * not explicit defined.
+ */
+#ifndef OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
+#define OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#endif
+
+/**
+ * @def OPENTHREAD_POSIX_CONFIG_FACTORY_CONFIG_FILE
+ *
+ * Define the path of the factory config file.
+ *
+ * Note: The factory config file contains the persist data that configured by the factory. And it won't be changed
+ *       after a device firmware update OTA is done.
+ *
+ */
+#ifndef OPENTHREAD_POSIX_CONFIG_FACTORY_CONFIG_FILE
+#define OPENTHREAD_POSIX_CONFIG_FACTORY_CONFIG_FILE "src/posix/platform/openthread.conf.example"
+#endif
+
+/**
+ * @def OPENTHREAD_POSIX_CONFIG_PRODUCT_CONFIG_FILE
+ *
+ * Define the path of the product config file.
+ *
+ */
+#ifndef OPENTHREAD_POSIX_CONFIG_PRODUCT_CONFIG_FILE
+#define OPENTHREAD_POSIX_CONFIG_PRODUCT_CONFIG_FILE "src/posix/platform/openthread.conf.example"
+#endif
+
+/**
+ * @def OPENTHREAD_POSIX_CONFIG_RCP_TIME_SYNC_INTERVAL
+ *
+ * This setting configures the interval (in units of microseconds) for host-rcp
+ * time sync. The host will recalculate the time offset between host and RCP
+ * every interval.
+ *
+ */
+#ifndef OPENTHREAD_POSIX_CONFIG_RCP_TIME_SYNC_INTERVAL
+#define OPENTHREAD_POSIX_CONFIG_RCP_TIME_SYNC_INTERVAL (60 * 1000 * 1000)
+#endif
 #endif // OPENTHREAD_PLATFORM_CONFIG_H_
diff --git a/src/posix/platform/openthread.conf.example b/src/posix/platform/openthread.conf.example
new file mode 100644
index 0000000..83a2908
--- /dev/null
+++ b/src/posix/platform/openthread.conf.example
@@ -0,0 +1,24 @@
+#
+# Sample configuration file
+#
+# Modify this to use your own configurations!
+#
+
+# Target power table entries.
+# target_power=<RegulatoryDomain>,<ChannelStart>,<ChannelEnd>,<TargetPower>
+target_power=ETSI,11,26,1000
+target_power=FCC,11,14,1700
+target_power=FCC,15,24,2000
+target_power=FCC,25,26,1600
+
+# Region domain mapping table entries.
+# region_domain_mapping=<RegulatoryDomain>,<Region>,<Region>,...
+region_domain_mapping=FCC,AU,CA,CL,CO,IN,MX,PE,TW,US
+region_domain_mapping=ETSI,WW
+
+# Power calibration table entries.
+# calibrated_power=<ChannelStart>,<ChannelEnd>,<ActualPower>,<RawPowerSetting>
+calibrated_power=11,25,1900,112233
+calibrated_power=11,25,1000,223344
+calibrated_power=26,26,1500,334455
+calibrated_power=26,26,700,445566
diff --git a/src/posix/platform/platform-posix.h b/src/posix/platform/platform-posix.h
index 919a319..3f76dcd 100644
--- a/src/posix/platform/platform-posix.h
+++ b/src/posix/platform/platform-posix.h
@@ -49,6 +49,7 @@
 #include <openthread/instance.h>
 #include <openthread/ip6.h>
 #include <openthread/logging.h>
+#include <openthread/nat64.h>
 #include <openthread/openthread-system.h>
 #include <openthread/platform/time.h>
 
@@ -93,12 +94,6 @@
     uint8_t  mData[OT_EVENT_DATA_MAX_SIZE];
 } OT_TOOL_PACKED_END;
 
-struct RadioProcessContext
-{
-    const fd_set *mReadFdSet;
-    const fd_set *mWriteFdSet;
-};
-
 /**
  * This function initializes the alarm service used by OpenThread.
  *
@@ -183,23 +178,18 @@
 /**
  * This function updates the file descriptor sets with file descriptors used by the radio driver.
  *
- * @param[in,out]  aReadFdSet   A pointer to the read file descriptors.
- * @param[in,out]  aWriteFdSet  A pointer to the write file descriptors.
- * @param[in,out]  aMaxFd       A pointer to the max file descriptor.
- * @param[in,out]  aTimeout     A pointer to the timeout.
+ * @param[in]   aContext    A pointer to the mainloop context.
  *
  */
-void platformRadioUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, int *aMaxFd, struct timeval *aTimeout);
+void platformRadioUpdateFdSet(otSysMainloopContext *aContext);
 
 /**
  * This function performs radio driver processing.
  *
- * @param[in]   aInstance       A pointer to the OpenThread instance.
- * @param[in]   aReadFdSet      A pointer to the read file descriptors.
- * @param[in]   aWriteFdSet     A pointer to the write file descriptors.
+ * @param[in]   aContext    A pointer to the mainloop context.
  *
  */
-void platformRadioProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet);
+void platformRadioProcess(otInstance *aInstance, const otSysMainloopContext *aContext);
 
 /**
  * This function initializes the random number service used by OpenThread.
@@ -218,22 +208,18 @@
 /**
  * This function updates the file descriptor sets with file descriptors used by the UART driver.
  *
- * @param[in,out]  aReadFdSet   A pointer to the read file descriptors.
- * @param[in,out]  aWriteFdSet  A pointer to the write file descriptors.
- * @param[in,out]  aMaxFd       A pointer to the max file descriptor.
+ * @param[in]   aContext    A pointer to the mainloop context.
  *
  */
-void platformUartUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, fd_set *aErrorFdSet, int *aMaxFd);
+void platformUartUpdateFdSet(otSysMainloopContext *aContext);
 
 /**
  * This function performs radio driver processing.
  *
- * @param[in]   aReadFdSet      A pointer to the read file descriptors.
- * @param[in]   aWriteFdSet     A pointer to the write file descriptors.
- * @param[in]   aErrorFdSet     A pointer to the error file descriptors.
+ * @param[in]   aContext    A pointer to the mainloop context.
  *
  */
-void platformUartProcess(const fd_set *aReadFdSet, const fd_set *aWriteFdSet, const fd_set *aErrorFdSet);
+void platformUartProcess(const otSysMainloopContext *aContext);
 
 /**
  * This function initializes platform netif.
@@ -243,7 +229,7 @@
  * @param[in]   aInterfaceName  A pointer to Thread network interface name.
  *
  */
-void platformNetifInit(const char *aInterfaceName);
+void platformNetifInit(otPlatformConfig *aPlatformConfig);
 
 /**
  * This function sets up platform netif.
@@ -274,23 +260,18 @@
 /**
  * This function updates the file descriptor sets with file descriptors used by platform netif module.
  *
- * @param[in,out]  aReadFdSet    A pointer to the read file descriptors.
- * @param[in,out]  aWriteFdSet   A pointer to the write file descriptors.
- * @param[in,out]  aErrorFdSet   A pointer to the error file descriptors.
- * @param[in,out]  aMaxFd        A pointer to the max file descriptor.
+ * @param[in,out]  aContext  A pointer to the mainloop context.
  *
  */
-void platformNetifUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, fd_set *aErrorFdSet, int *aMaxFd);
+void platformNetifUpdateFdSet(otSysMainloopContext *aContext);
 
 /**
  * This function performs platform netif processing.
  *
- * @param[in]   aReadFdSet      A pointer to the read file descriptors.
- * @param[in]   aWriteFdSet     A pointer to the write file descriptors.
- * @param[in]   aErrorFdSet     A pointer to the error file descriptors.
+ * @param[in]  aContext  A pointer to the mainloop context.
  *
  */
-void platformNetifProcess(const fd_set *aReadFdSet, const fd_set *aWriteFdSet, const fd_set *aErrorFdSet);
+void platformNetifProcess(const otSysMainloopContext *aContext);
 
 /**
  * This function performs notifies state changes to platform netif.
@@ -318,32 +299,19 @@
 /**
  * This function performs virtual time simulation processing.
  *
- * @param[in]   aInstance       A pointer to the OpenThread instance.
- * @param[in]   aReadFdSet      A pointer to the read file descriptors.
- * @param[in]   aWriteFdSet     A pointer to the write file descriptors.
+ * @param[in]  aContext  A pointer to the mainloop context.
  *
  */
-void virtualTimeProcess(otInstance *  aInstance,
-                        const fd_set *aReadFdSet,
-                        const fd_set *aWriteFdSet,
-                        const fd_set *aErrorFdSet);
+void virtualTimeProcess(otInstance *aInstance, const otSysMainloopContext *aContext);
 
 /**
  * This function updates the file descriptor sets with file descriptors
  * used by the virtual time simulation.
  *
- * @param[in,out]  aReadFdSet   A pointer to the read file descriptors.
- * @param[in,out]  aWriteFdSet  A pointer to the write file descriptors.
- * @param[in,out]  aErrorFdSet  A pointer to the error file descriptors.
- * @param[in,out]  aMaxFd       A pointer to the max file descriptor.
- * @param[in,out]  aTimeout     A pointer to the timeout.
+ * @param[in,out]  aContext  A pointer to the mainloop context.
  *
  */
-void virtualTimeUpdateFdSet(fd_set *        aReadFdSet,
-                            fd_set *        aWriteFdSet,
-                            fd_set *        aErrorFdSet,
-                            int *           aMaxFd,
-                            struct timeval *aTimeout);
+void virtualTimeUpdateFdSet(otSysMainloopContext *aContext);
 
 /**
  * This function sends radio spinel event of virtual time simulation.
@@ -402,23 +370,18 @@
 /**
  * This function updates the file descriptor sets with file descriptors used by the TREL driver.
  *
- * @param[in,out]  aReadFdSet   A pointer to the read file descriptors.
- * @param[in,out]  aWriteFdSet  A pointer to the write file descriptors.
- * @param[in,out]  aMaxFd       A pointer to the max file descriptor.
- * @param[in,out]  aTimeout     A pointer to the timeout.
+ * @param[in,out]  aContext  A pointer to the mainloop context.
  *
  */
-void platformTrelUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, int *aMaxFd, struct timeval *aTimeout);
+void platformTrelUpdateFdSet(otSysMainloopContext *aContext);
 
 /**
  * This function performs TREL driver processing.
  *
- * @param[in]   aInstance       A pointer to the OpenThread instance.
- * @param[in]   aReadFdSet      A pointer to the read file descriptors.
- * @param[in]   aWriteFdSet     A pointer to the write file descriptors.
+ * @param[in]  aContext  A pointer to the mainloop context.
  *
  */
-void platformTrelProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet);
+void platformTrelProcess(otInstance *aInstance, const otSysMainloopContext *aContext);
 
 /**
  * This function creates a socket with SOCK_CLOEXEC flag set.
@@ -448,6 +411,11 @@
 extern unsigned int gNetifIndex;
 
 /**
+ * The CIDR for NAT64
+ */
+extern otIp4Cidr gNat64Cidr;
+
+/**
  * This function initializes platform Backbone network.
  *
  * @note This function is called before OpenThread instance is created.
@@ -518,6 +486,12 @@
  */
 bool platformInfraIfIsRunning(void);
 
+/**
+ * This function initializes backtrace module.
+ *
+ */
+void platformBacktraceInit(void);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/posix/platform/power.cpp b/src/posix/platform/power.cpp
new file mode 100644
index 0000000..ea832a3
--- /dev/null
+++ b/src/posix/platform/power.cpp
@@ -0,0 +1,126 @@
+/*
+ *  Copyright (c) 2022, 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 strain 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 "power.hpp"
+#include "common/code_utils.hpp"
+#include "utils/parse_cmdline.hpp"
+
+namespace ot {
+namespace Power {
+
+otError Domain::Set(const char *aDomain)
+{
+    otError  error  = OT_ERROR_NONE;
+    uint16_t length = static_cast<uint16_t>(strlen(aDomain));
+
+    VerifyOrExit(length <= kDomainSize, error = OT_ERROR_INVALID_ARGS);
+    memcpy(m8, aDomain, length);
+    m8[length] = '\0';
+
+exit:
+    return error;
+}
+
+otError TargetPower::FromString(char *aString)
+{
+    otError error = OT_ERROR_NONE;
+    char   *str;
+
+    VerifyOrExit((str = strtok(aString, ",")) != nullptr, error = OT_ERROR_PARSE);
+    SuccessOrExit(error = Utils::CmdLineParser::ParseAsUint8(str, mChannelStart));
+
+    VerifyOrExit((str = strtok(nullptr, ",")) != nullptr, error = OT_ERROR_PARSE);
+    SuccessOrExit(error = Utils::CmdLineParser::ParseAsUint8(str, mChannelEnd));
+
+    VerifyOrExit((str = strtok(nullptr, ",")) != nullptr, error = OT_ERROR_PARSE);
+    SuccessOrExit(error = Utils::CmdLineParser::ParseAsInt16(str, mTargetPower));
+
+exit:
+    return error;
+}
+
+TargetPower::InfoString TargetPower::ToString(void) const
+{
+    InfoString string;
+
+    string.Append("%u,%u,%d", mChannelStart, mChannelEnd, mTargetPower);
+
+    return string;
+}
+
+otError RawPowerSetting::Set(const char *aRawPowerSetting)
+{
+    otError  error;
+    uint16_t length = sizeof(mData);
+
+    SuccessOrExit(error = ot::Utils::CmdLineParser::ParseAsHexString(aRawPowerSetting, length, mData));
+    mLength = static_cast<uint8_t>(length);
+
+exit:
+    return error;
+}
+
+RawPowerSetting::InfoString RawPowerSetting::ToString(void) const
+{
+    InfoString string;
+
+    string.AppendHexBytes(mData, mLength);
+
+    return string;
+}
+
+otError CalibratedPower::FromString(char *aString)
+{
+    otError error = OT_ERROR_NONE;
+    char   *str;
+    char   *pSave;
+
+    VerifyOrExit((str = strtok_r(aString, ",", &pSave)) != nullptr, error = OT_ERROR_PARSE);
+    SuccessOrExit(error = Utils::CmdLineParser::ParseAsUint8(str, mChannelStart));
+
+    VerifyOrExit((str = strtok_r(nullptr, ",", &pSave)) != nullptr, error = OT_ERROR_PARSE);
+    SuccessOrExit(error = Utils::CmdLineParser::ParseAsUint8(str, mChannelEnd));
+
+    VerifyOrExit((str = strtok_r(nullptr, ",", &pSave)) != nullptr, error = OT_ERROR_PARSE);
+    SuccessOrExit(error = Utils::CmdLineParser::ParseAsInt16(str, mActualPower));
+    SuccessOrExit(error = mRawPowerSetting.Set(pSave));
+
+exit:
+    return error;
+}
+
+CalibratedPower::InfoString CalibratedPower::ToString(void) const
+{
+    InfoString string;
+
+    string.Append("%u,%u,%d,%s", mChannelStart, mChannelEnd, mActualPower, mRawPowerSetting.ToString().AsCString());
+
+    return string;
+}
+} // namespace Power
+} // namespace ot
diff --git a/src/posix/platform/power.hpp b/src/posix/platform/power.hpp
new file mode 100644
index 0000000..dbc92e0
--- /dev/null
+++ b/src/posix/platform/power.hpp
@@ -0,0 +1,289 @@
+/*
+ *  Copyright (c) 2022, 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 strain 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.
+ */
+
+#ifndef POSIX_PLATFORM_POWER_H
+#define POSIX_PLATFORM_POWER_H
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <openthread/error.h>
+#include <openthread/platform/radio.h>
+#include "common/string.hpp"
+
+namespace ot {
+namespace Power {
+
+class Domain
+{
+public:
+    Domain(void) { m8[0] = '\0'; }
+
+    /**
+     * This method sets the regulatory domain from a given null terminated C string.
+     *
+     * @param[in] aDomain   A regulatory domain name C string.
+     *
+     * @retval OT_ERROR_NONE           Successfully set the regulatory domain.
+     * @retval OT_ERROR_INVALID_ARGS   Given regulatory domain is too long.
+     *
+     */
+    otError Set(const char *aDomain);
+
+    /**
+     * This method overloads operator `==` to evaluate whether or not two `Domain` instances are equal.
+     *
+     * @param[in]  aOther  The other `Domain` instance to compare with.
+     *
+     * @retval TRUE   If the two `Domain` instances are equal.
+     * @retval FALSE  If the two `Domain` instances not equal.
+     *
+     */
+    bool operator==(const Domain &aOther) const { return strcmp(m8, aOther.m8) == 0; }
+
+    /**
+     * This method overloads operator `!=` to evaluate whether or not the `Domain` is unequal to a given C string.
+     *
+     * @param[in]  aCString  A C string to compare with. Can be `nullptr` which then returns 'TRUE'.
+     *
+     * @retval TRUE   If the two regulatory domains are not equal.
+     * @retval FALSE  If the two regulatory domains are equal.
+     *
+     */
+    bool operator!=(const char *aCString) const { return (aCString == nullptr) ? true : strcmp(m8, aCString) != 0; }
+
+    /**
+     * This method gets the regulatory domain as a null terminated C string.
+     *
+     * @returns The regulatory domain as a null terminated C string array.
+     *
+     */
+    const char *AsCString(void) const { return m8; }
+
+private:
+    static constexpr uint8_t kDomainSize = 8;
+    char                     m8[kDomainSize + 1];
+};
+
+class TargetPower
+{
+public:
+    static constexpr uint16_t       kInfoStringSize = 12; ///< Recommended buffer size to use with `ToString()`.
+    typedef String<kInfoStringSize> InfoString;
+
+    /**
+     * This method parses an target power string.
+     *
+     * The string MUST follow the format: "<channel_start>,<channel_end>,<target_power>".
+     * For example, "11,26,2000"
+     *
+     * @param[in]  aString   A pointer to the null-terminated string.
+     *
+     * @retval OT_ERROR_NONE   Successfully parsed the target power string.
+     * @retval OT_ERROR_PARSE  Failed to parse the target power string.
+     *
+     */
+    otError FromString(char *aString);
+
+    /**
+     * This method returns the start channel.
+     *
+     * @returns The channel.
+     *
+     */
+    uint8_t GetChannelStart(void) const { return mChannelStart; }
+
+    /**
+     * This method returns the end channel.
+     *
+     * @returns The channel.
+     *
+     */
+    uint8_t GetChannelEnd(void) const { return mChannelEnd; }
+
+    /**
+     * This method returns the target power.
+     *
+     * @returns The target power, in 0.01 dBm.
+     *
+     */
+    int16_t GetTargetPower(void) const { return mTargetPower; }
+
+    /**
+     * This method converts the target power into a human-readable string.
+     *
+     * @returns  An `InfoString` object representing the target power.
+     *
+     */
+    InfoString ToString(void) const;
+
+private:
+    uint8_t mChannelStart;
+    uint8_t mChannelEnd;
+    int16_t mTargetPower;
+};
+
+class RawPowerSetting
+{
+public:
+    // Recommended buffer size to use with `ToString()`.
+    static constexpr uint16_t kInfoStringSize = OPENTHREAD_CONFIG_POWER_CALIBRATION_RAW_POWER_SETTING_SIZE * 2 + 1;
+    typedef String<kInfoStringSize> InfoString;
+
+    /**
+     * This method sets the raw power setting from a given null terminated hex C string.
+     *
+     * @param[in] aRawPowerSetting  A raw power setting hex C string.
+     *
+     * @retval OT_ERROR_NONE           Successfully set the raw power setting.
+     * @retval OT_ERROR_INVALID_ARGS   The given raw power setting is too long.
+     *
+     */
+    otError Set(const char *aRawPowerSetting);
+
+    /**
+     * This method converts the raw power setting into a human-readable string.
+     *
+     * @returns  An `InfoString` object representing the calibrated power.
+     *
+     */
+    InfoString ToString(void) const;
+
+    const uint8_t *GetData(void) const { return mData; }
+    uint16_t       GetLength(void) const { return mLength; }
+
+private:
+    static constexpr uint16_t kMaxRawPowerSettingSize = OPENTHREAD_CONFIG_POWER_CALIBRATION_RAW_POWER_SETTING_SIZE;
+
+    uint8_t  mData[kMaxRawPowerSettingSize];
+    uint16_t mLength;
+};
+
+class CalibratedPower
+{
+public:
+    // Recommended buffer size to use with `ToString()`.
+    static constexpr uint16_t       kInfoStringSize = 20 + RawPowerSetting::kInfoStringSize;
+    typedef String<kInfoStringSize> InfoString;
+
+    /**
+     * This method parses an calibrated power string.
+     *
+     * The string MUST follow the format: "<channel_start>,<channel_end>,<actual_power>,<raw_power_setting>".
+     * For example, "11,26,2000,1122aabb"
+     *
+     * @param[in]  aString   A pointer to the null-terminated string.
+     *
+     * @retval OT_ERROR_NONE   Successfully parsed the calibrated power string.
+     * @retval OT_ERROR_PARSE  Failed to parse the calibrated power string.
+     *
+     */
+    otError FromString(char *aString);
+
+    /**
+     * This method returns the start channel.
+     *
+     * @returns The channel.
+     *
+     */
+    uint8_t GetChannelStart(void) const { return mChannelStart; }
+
+    /**
+     * This method sets the start channel.
+     *
+     * @param[in]  aChannelStart  The start channel.
+     *
+     */
+    void SetChannelStart(uint8_t aChannelStart) { mChannelStart = aChannelStart; }
+
+    /**
+     * This method returns the end channel.
+     *
+     * @returns The channel.
+     *
+     */
+    uint8_t GetChannelEnd(void) const { return mChannelEnd; }
+
+    /**
+     * This method sets the end channel.
+     *
+     * @param[in]  aChannelEnd  The end channel.
+     *
+     */
+    void SetChannelEnd(uint8_t aChannelEnd) { mChannelEnd = aChannelEnd; }
+
+    /**
+     * This method returns the actual power.
+     *
+     * @returns The actual measured power, in 0.01 dBm.
+     *
+     */
+    int16_t GetActualPower(void) const { return mActualPower; }
+
+    /**
+     * This method sets the actual channel.
+     *
+     * @param[in]  aActualPower  The actual power in 0.01 dBm.
+     *
+     */
+    void SetActualPower(int16_t aActualPower) { mActualPower = aActualPower; }
+
+    /**
+     * This method returns the raw power setting.
+     *
+     * @returns A reference to the raw power setting.
+     *
+     */
+    const RawPowerSetting &GetRawPowerSetting(void) const { return mRawPowerSetting; }
+
+    /**
+     * This method sets the raw power setting.
+     *
+     * @param[in]  aRawPowerSetting  The raw power setting.
+     *
+     */
+    void SetRawPowerSetting(const RawPowerSetting &aRawPowerSetting) { mRawPowerSetting = aRawPowerSetting; }
+
+    /**
+     * This method converts the calibrated power into a human-readable string.
+     *
+     * @returns  An `InfoString` object representing the calibrated power.
+     *
+     */
+    InfoString ToString(void) const;
+
+private:
+    uint8_t         mChannelStart;
+    uint8_t         mChannelEnd;
+    int16_t         mActualPower;
+    RawPowerSetting mRawPowerSetting;
+};
+} // namespace Power
+} // namespace ot
+#endif // POSIX_PLATFORM_POWER_H
diff --git a/src/posix/platform/power_updater.cpp b/src/posix/platform/power_updater.cpp
new file mode 100644
index 0000000..d3b45bb
--- /dev/null
+++ b/src/posix/platform/power_updater.cpp
@@ -0,0 +1,180 @@
+/*
+ *  Copyright (c) 2022, 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 strain 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 "power_updater.hpp"
+
+#include "platform-posix.h"
+#include <openthread/platform/radio.h>
+#include "lib/platform/exit_code.h"
+
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+
+namespace ot {
+namespace Posix {
+
+otError PowerUpdater::SetRegion(uint16_t aRegionCode)
+{
+    otError            error    = OT_ERROR_NONE;
+    int                iterator = 0;
+    Power::Domain      domain;
+    Power::TargetPower targetPower;
+
+    if (GetDomain(aRegionCode, domain) != OT_ERROR_NONE)
+    {
+        // If failed to find the domain for the region, use the world wide region as the default region.
+        VerifyOrExit(GetDomain(kRegionCodeWorldWide, domain) == OT_ERROR_NONE, error = OT_ERROR_FAILED);
+    }
+
+    while (GetNextTargetPower(domain, iterator, targetPower) == OT_ERROR_NONE)
+    {
+        otLogInfoPlat("Update target power: %s\r\n", targetPower.ToString().AsCString());
+
+        for (uint8_t ch = targetPower.GetChannelStart(); ch <= targetPower.GetChannelEnd(); ch++)
+        {
+            SuccessOrExit(error = otPlatRadioSetChannelTargetPower(gInstance, ch, targetPower.GetTargetPower()));
+        }
+    }
+
+    SuccessOrExit(error = UpdateCalibratedPower());
+
+    mRegionCode = aRegionCode;
+
+exit:
+    if (error == OT_ERROR_NONE)
+    {
+        otLogInfoPlat("Set region \"%c%c\" successfully", (aRegionCode >> 8) & 0xff, (aRegionCode & 0xff));
+    }
+    else
+    {
+        otLogCritPlat("Set region \"%c%c\" failed, Error: %s", (aRegionCode >> 8) & 0xff, (aRegionCode & 0xff),
+                      otThreadErrorToString(error));
+    }
+
+    return error;
+}
+
+otError PowerUpdater::UpdateCalibratedPower(void)
+{
+    otError                error    = OT_ERROR_NONE;
+    int                    iterator = 0;
+    char                   value[kMaxValueSize];
+    Power::CalibratedPower calibratedPower;
+    ConfigFile            *calibrationFile = &mFactoryConfigFile;
+
+    SuccessOrExit(error = otPlatRadioClearCalibratedPowers(gInstance));
+
+    // If the distribution of output power is large, the factory needs to measure the power calibration data
+    // for each device individually, and the power calibration data will be written to the factory config file.
+    // Otherwise, the power calibration data can be pre-configured in the product config file.
+    if (calibrationFile->Get(kKeyCalibratedPower, iterator, value, sizeof(value)) != OT_ERROR_NONE)
+    {
+        calibrationFile = &mProductConfigFile;
+    }
+
+    iterator = 0;
+    while (calibrationFile->Get(kKeyCalibratedPower, iterator, value, sizeof(value)) == OT_ERROR_NONE)
+    {
+        SuccessOrExit(error = calibratedPower.FromString(value));
+        otLogInfoPlat("Update calibrated power: %s\r\n", calibratedPower.ToString().AsCString());
+
+        for (uint8_t ch = calibratedPower.GetChannelStart(); ch <= calibratedPower.GetChannelEnd(); ch++)
+        {
+            SuccessOrExit(error = otPlatRadioAddCalibratedPower(gInstance, ch, calibratedPower.GetActualPower(),
+                                                                calibratedPower.GetRawPowerSetting().GetData(),
+                                                                calibratedPower.GetRawPowerSetting().GetLength()));
+        }
+    }
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        otLogCritPlat("Update calibrated power table failed, Error: %s", otThreadErrorToString(error));
+    }
+
+    return error;
+}
+
+otError PowerUpdater::GetDomain(uint16_t aRegionCode, Power::Domain &aDomain)
+{
+    otError error    = OT_ERROR_NOT_FOUND;
+    int     iterator = 0;
+    char    value[kMaxValueSize];
+    char   *str;
+
+    while (mProductConfigFile.Get(kKeyRegionDomainMapping, iterator, value, sizeof(value)) == OT_ERROR_NONE)
+    {
+        if ((str = strtok(value, kCommaDelimiter)) == nullptr)
+        {
+            continue;
+        }
+
+        while ((str = strtok(nullptr, kCommaDelimiter)) != nullptr)
+        {
+            if ((strlen(str) == 2) && (StringToRegionCode(str) == aRegionCode))
+            {
+                ExitNow(error = aDomain.Set(value));
+            }
+        }
+    }
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        otLogCritPlat("Get domain failed, Error: %s", otThreadErrorToString(error));
+    }
+
+    return error;
+}
+
+otError PowerUpdater::GetNextTargetPower(const Power::Domain &aDomain, int &aIterator, Power::TargetPower &aTargetPower)
+{
+    otError error = OT_ERROR_NOT_FOUND;
+    char    value[kMaxValueSize];
+    char   *domain;
+    char   *psave;
+
+    while (mProductConfigFile.Get(kKeyTargetPower, aIterator, value, sizeof(value)) == OT_ERROR_NONE)
+    {
+        if (((domain = strtok_r(value, kCommaDelimiter, &psave)) == nullptr) || (aDomain != domain))
+        {
+            continue;
+        }
+
+        if ((error = aTargetPower.FromString(psave)) != OT_ERROR_NONE)
+        {
+            otLogCritPlat("Read target power failed, Error: %s", otThreadErrorToString(error));
+        }
+        break;
+    }
+
+    return error;
+}
+
+} // namespace Posix
+} // namespace ot
+#endif // OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
diff --git a/src/posix/platform/power_updater.hpp b/src/posix/platform/power_updater.hpp
new file mode 100644
index 0000000..fb12c23
--- /dev/null
+++ b/src/posix/platform/power_updater.hpp
@@ -0,0 +1,116 @@
+/*
+ *  Copyright (c) 2022, 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 strain 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.
+ */
+
+#ifndef POSIX_PLATFORM_POWER_UPDATER_HPP_
+#define POSIX_PLATFORM_POWER_UPDATER_HPP_
+
+#include "openthread-posix-config.h"
+
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+
+#include <stdio.h>
+#include <string.h>
+
+#include <openthread/error.h>
+#include <openthread/logging.h>
+#include <openthread/platform/radio.h>
+
+#include "config_file.hpp"
+#include "power.hpp"
+#include "common/code_utils.hpp"
+
+namespace ot {
+namespace Posix {
+
+/**
+ * This class updates the target power table and calibrated power table to the RCP.
+ *
+ */
+class PowerUpdater
+{
+public:
+    PowerUpdater(void)
+        : mFactoryConfigFile(kFactoryConfigFile)
+        , mProductConfigFile(kProductConfigFile)
+        , mRegionCode(0)
+    {
+    }
+
+    /**
+     * Set the region code.
+     *
+     * The radio region format is the 2-bytes ascii representation of the
+     * ISO 3166 alpha-2 code.
+     *
+     * @param[in]  aRegionCode  The radio region.
+     *
+     * @retval  OT_ERROR_NONE             Successfully set region code.
+     * @retval  OT_ERROR_FAILED           Failed to set the region code.
+     *
+     */
+    otError SetRegion(uint16_t aRegionCode);
+
+    /**
+     * Get the region code.
+     *
+     * The radio region format is the 2-bytes ascii representation of the
+     * ISO 3166 alpha-2 code.
+     *
+     * @returns  The region code.
+     *
+     */
+    uint16_t GetRegion(void) const { return mRegionCode; }
+
+private:
+    const char               *kFactoryConfigFile      = OPENTHREAD_POSIX_CONFIG_FACTORY_CONFIG_FILE;
+    const char               *kProductConfigFile      = OPENTHREAD_POSIX_CONFIG_PRODUCT_CONFIG_FILE;
+    const char               *kKeyCalibratedPower     = "calibrated_power";
+    const char               *kKeyTargetPower         = "target_power";
+    const char               *kKeyRegionDomainMapping = "region_domain_mapping";
+    const char               *kCommaDelimiter         = ",";
+    static constexpr uint16_t kMaxValueSize           = 512;
+    static constexpr uint16_t kRegionCodeWorldWide    = 0x5757; // Region Code: "WW"
+
+    uint16_t StringToRegionCode(const char *aString) const
+    {
+        return static_cast<uint16_t>(((aString[0] & 0xFF) << 8) | ((aString[1] & 0xFF) << 0));
+    }
+    otError GetDomain(uint16_t aRegionCode, Power::Domain &aDomain);
+    otError GetNextTargetPower(const Power::Domain &aDomain, int &aIterator, Power::TargetPower &aTargetPower);
+    otError UpdateCalibratedPower(void);
+
+    ConfigFile mFactoryConfigFile;
+    ConfigFile mProductConfigFile;
+    uint16_t   mRegionCode;
+};
+
+} // namespace Posix
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+#endif // POSIX_PLATFORM_POWER_UPDATER_HPP_
diff --git a/src/posix/platform/radio.cpp b/src/posix/platform/radio.cpp
index 8fb63bf..17d9c8a 100644
--- a/src/posix/platform/radio.cpp
+++ b/src/posix/platform/radio.cpp
@@ -35,32 +35,36 @@
 
 #include <string.h>
 
+#include <openthread/logging.h>
+
 #include "common/code_utils.hpp"
 #include "common/new.hpp"
 #include "lib/spinel/radio_spinel.hpp"
 #include "posix/platform/radio.hpp"
+#include "utils/parse_cmdline.hpp"
 
 #if OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_UART
 #include "hdlc_interface.hpp"
 
-#if OPENTHREAD_POSIX_VIRTUAL_TIME
-static ot::Spinel::RadioSpinel<ot::Posix::HdlcInterface, VirtualTimeEvent> sRadioSpinel;
-#else
-static ot::Spinel::RadioSpinel<ot::Posix::HdlcInterface, RadioProcessContext> sRadioSpinel;
-#endif // OPENTHREAD_POSIX_VIRTUAL_TIME
+static ot::Spinel::RadioSpinel<ot::Posix::HdlcInterface> sRadioSpinel;
 #elif OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_SPI
 #include "spi_interface.hpp"
 
-static ot::Spinel::RadioSpinel<ot::Posix::SpiInterface, RadioProcessContext> sRadioSpinel;
+static ot::Spinel::RadioSpinel<ot::Posix::SpiInterface> sRadioSpinel;
 #elif OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_VENDOR
 #include "vendor_interface.hpp"
 
-static ot::Spinel::RadioSpinel<ot::Posix::VendorInterface, RadioProcessContext> sRadioSpinel;
+static ot::Spinel::RadioSpinel<ot::Posix::VendorInterface> sRadioSpinel;
 #else
 #error "OPENTHREAD_POSIX_CONFIG_RCP_BUS only allows OT_POSIX_RCP_BUS_UART, OT_POSIX_RCP_BUS_SPI and " \
     "OT_POSIX_RCP_BUS_VENDOR!"
 #endif
 
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+#include "power_updater.hpp"
+static ot::Posix::PowerUpdater sPowerUpdater;
+#endif
+
 namespace ot {
 namespace Posix {
 
@@ -133,7 +137,7 @@
 
         VerifyOrDie(strnlen(region, 3) == 2, OT_EXIT_INVALID_ARGUMENTS);
         regionCode = static_cast<uint16_t>(static_cast<uint16_t>(region[0]) << 8) + static_cast<uint16_t>(region[1]);
-        SuccessOrDie(sRadioSpinel.SetRadioRegion(regionCode));
+        SuccessOrDie(otPlatRadioSetRegion(gInstance, regionCode));
     }
 
 #if OPENTHREAD_POSIX_CONFIG_MAX_POWER_TABLE_ENABLE
@@ -141,7 +145,7 @@
     if (maxPowerTable != nullptr)
     {
         constexpr int8_t kPowerDefault = 30; // Default power 1 watt (30 dBm).
-        const char *     str           = nullptr;
+        const char      *str           = nullptr;
         uint8_t          channel       = ot::Radio::kChannelMin;
         int8_t           power         = kPowerDefault;
         otError          error;
@@ -151,10 +155,15 @@
         {
             power = static_cast<int8_t>(strtol(str, nullptr, 0));
             error = sRadioSpinel.SetChannelMaxTransmitPower(channel, power);
-            if (error != OT_ERROR_NONE && error != OT_ERROR_NOT_FOUND)
+            if (error != OT_ERROR_NONE && error != OT_ERROR_NOT_IMPLEMENTED)
             {
                 DieNow(OT_ERROR_FAILED);
             }
+            else if (error == OT_ERROR_NOT_IMPLEMENTED)
+            {
+                otLogWarnPlat("The RCP doesn't support setting the max transmit power");
+            }
+
             ++channel;
         }
 
@@ -162,10 +171,15 @@
         while (channel <= ot::Radio::kChannelMax)
         {
             error = sRadioSpinel.SetChannelMaxTransmitPower(channel, power);
-            if (error != OT_ERROR_NONE && error != OT_ERROR_NOT_FOUND)
+            if (error != OT_ERROR_NONE && error != OT_ERROR_NOT_IMPLEMENTED)
             {
                 DieNow(OT_ERROR_FAILED);
             }
+            else if (error == OT_ERROR_NOT_IMPLEMENTED)
+            {
+                otLogWarnPlat("The RCP doesn't support setting the max transmit power");
+            }
+
             ++channel;
         }
 
@@ -183,13 +197,12 @@
 #endif // OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE
 }
 
+void *Radio::GetSpinelInstance(void) { return &sRadioSpinel; }
+
 } // namespace Posix
 } // namespace ot
 
-void platformRadioDeinit(void)
-{
-    sRadioSpinel.Deinit();
-}
+void platformRadioDeinit(void) { sRadioSpinel.Deinit(); }
 
 void otPlatRadioGetIeeeEui64(otInstance *aInstance, uint8_t *aIeeeEui64)
 {
@@ -234,10 +247,7 @@
     return sRadioSpinel.IsEnabled();
 }
 
-otError otPlatRadioEnable(otInstance *aInstance)
-{
-    return sRadioSpinel.Enable(aInstance);
-}
+otError otPlatRadioEnable(otInstance *aInstance) { return sRadioSpinel.Enable(aInstance); }
 
 otError otPlatRadioDisable(otInstance *aInstance)
 {
@@ -299,7 +309,7 @@
     return sRadioSpinel.IsPromiscuous();
 }
 
-void platformRadioUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, int *aMaxFd, struct timeval *aTimeout)
+void platformRadioUpdateFdSet(otSysMainloopContext *aContext)
 {
     uint64_t now      = otPlatTimeGet();
     uint64_t deadline = sRadioSpinel.GetNextRadioTimeRecalcStart();
@@ -318,24 +328,25 @@
     {
         uint64_t remain = deadline - now;
 
-        if (remain < (static_cast<uint64_t>(aTimeout->tv_sec) * US_PER_S + static_cast<uint64_t>(aTimeout->tv_usec)))
+        if (remain < (static_cast<uint64_t>(aContext->mTimeout.tv_sec) * US_PER_S +
+                      static_cast<uint64_t>(aContext->mTimeout.tv_usec)))
         {
-            aTimeout->tv_sec  = static_cast<time_t>(remain / US_PER_S);
-            aTimeout->tv_usec = static_cast<suseconds_t>(remain % US_PER_S);
+            aContext->mTimeout.tv_sec  = static_cast<time_t>(remain / US_PER_S);
+            aContext->mTimeout.tv_usec = static_cast<suseconds_t>(remain % US_PER_S);
         }
     }
     else
     {
-        aTimeout->tv_sec  = 0;
-        aTimeout->tv_usec = 0;
+        aContext->mTimeout.tv_sec  = 0;
+        aContext->mTimeout.tv_usec = 0;
     }
 
-    sRadioSpinel.GetSpinelInterface().UpdateFdSet(*aReadFdSet, *aWriteFdSet, *aMaxFd, *aTimeout);
+    sRadioSpinel.GetSpinelInterface().UpdateFdSet(aContext);
 
     if (sRadioSpinel.HasPendingFrame() || sRadioSpinel.IsTransmitDone())
     {
-        aTimeout->tv_sec  = 0;
-        aTimeout->tv_usec = 0;
+        aContext->mTimeout.tv_sec  = 0;
+        aContext->mTimeout.tv_usec = 0;
     }
 }
 
@@ -343,15 +354,14 @@
 void virtualTimeRadioSpinelProcess(otInstance *aInstance, const struct VirtualTimeEvent *aEvent)
 {
     OT_UNUSED_VARIABLE(aInstance);
-    sRadioSpinel.Process(*aEvent);
+    sRadioSpinel.Process(aEvent);
 }
 #else
-void platformRadioProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet)
+void platformRadioProcess(otInstance *aInstance, const otSysMainloopContext *aContext)
 {
     OT_UNUSED_VARIABLE(aInstance);
-    RadioProcessContext context = {aReadFdSet, aWriteFdSet};
 
-    sRadioSpinel.Process(context);
+    sRadioSpinel.Process(aContext);
 }
 #endif // OPENTHREAD_POSIX_VIRTUAL_TIME
 
@@ -493,8 +503,8 @@
 #if OPENTHREAD_CONFIG_DIAG_ENABLE
 otError otPlatDiagProcess(otInstance *aInstance,
                           uint8_t     aArgsLength,
-                          char *      aArgs[],
-                          char *      aOutput,
+                          char       *aArgs[],
+                          char       *aOutput,
                           size_t      aOutputMaxLen)
 {
     // deliver the platform specific diags commands to radio only ncp.
@@ -520,10 +530,7 @@
     return;
 }
 
-bool otPlatDiagModeGet(void)
-{
-    return sRadioSpinel.IsDiagEnabled();
-}
+bool otPlatDiagModeGet(void) { return sRadioSpinel.IsDiagEnabled(); }
 
 void otPlatDiagTxPowerSet(int8_t aTxPower)
 {
@@ -547,6 +554,195 @@
     return;
 }
 
+otError otPlatDiagGpioSet(uint32_t aGpio, bool aValue)
+{
+    otError error;
+    char    cmd[OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE];
+
+    snprintf(cmd, sizeof(cmd), "gpio set %d %d", aGpio, aValue);
+    SuccessOrExit(error = sRadioSpinel.PlatDiagProcess(cmd, nullptr, 0));
+
+exit:
+    return error;
+}
+
+otError otPlatDiagGpioGet(uint32_t aGpio, bool *aValue)
+{
+    otError error;
+    char    cmd[OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE];
+    char    output[OPENTHREAD_CONFIG_DIAG_OUTPUT_BUFFER_SIZE];
+    char   *str;
+
+    snprintf(cmd, sizeof(cmd), "gpio get %d", aGpio);
+    SuccessOrExit(error = sRadioSpinel.PlatDiagProcess(cmd, output, sizeof(output)));
+    VerifyOrExit((str = strtok(output, "\r")) != nullptr, error = OT_ERROR_FAILED);
+    *aValue = static_cast<bool>(atoi(str));
+
+exit:
+    return error;
+}
+
+otError otPlatDiagGpioSetMode(uint32_t aGpio, otGpioMode aMode)
+{
+    otError error;
+    char    cmd[OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE];
+
+    snprintf(cmd, sizeof(cmd), "gpio mode %d %s", aGpio, aMode == OT_GPIO_MODE_INPUT ? "in" : "out");
+    SuccessOrExit(error = sRadioSpinel.PlatDiagProcess(cmd, nullptr, 0));
+
+exit:
+    return error;
+}
+
+otError otPlatDiagGpioGetMode(uint32_t aGpio, otGpioMode *aMode)
+{
+    otError error;
+    char    cmd[OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE];
+    char    output[OPENTHREAD_CONFIG_DIAG_OUTPUT_BUFFER_SIZE];
+    char   *str;
+
+    snprintf(cmd, sizeof(cmd), "gpio mode %d", aGpio);
+    SuccessOrExit(error = sRadioSpinel.PlatDiagProcess(cmd, output, sizeof(output)));
+    VerifyOrExit((str = strtok(output, "\r")) != nullptr, error = OT_ERROR_FAILED);
+
+    if (strcmp(str, "in") == 0)
+    {
+        *aMode = OT_GPIO_MODE_INPUT;
+    }
+    else if (strcmp(str, "out") == 0)
+    {
+        *aMode = OT_GPIO_MODE_OUTPUT;
+    }
+    else
+    {
+        error = OT_ERROR_FAILED;
+    }
+
+exit:
+    return error;
+}
+
+otError otPlatDiagRadioGetPowerSettings(otInstance *aInstance,
+                                        uint8_t     aChannel,
+                                        int16_t    *aTargetPower,
+                                        int16_t    *aActualPower,
+                                        uint8_t    *aRawPowerSetting,
+                                        uint16_t   *aRawPowerSettingLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    static constexpr uint16_t kRawPowerStringSize = OPENTHREAD_CONFIG_POWER_CALIBRATION_RAW_POWER_SETTING_SIZE * 2 + 1;
+    static constexpr uint16_t kFmtStringSize      = 100;
+
+    otError error;
+    char    cmd[OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE];
+    char    output[OPENTHREAD_CONFIG_DIAG_OUTPUT_BUFFER_SIZE];
+    int     targetPower;
+    int     actualPower;
+    char    rawPowerSetting[kRawPowerStringSize];
+    char    fmt[kFmtStringSize];
+
+    assert((aTargetPower != nullptr) && (aActualPower != nullptr) && (aRawPowerSetting != nullptr) &&
+           (aRawPowerSettingLength != nullptr));
+
+    snprintf(cmd, sizeof(cmd), "powersettings %d", aChannel);
+    SuccessOrExit(error = sRadioSpinel.PlatDiagProcess(cmd, output, sizeof(output)));
+    snprintf(fmt, sizeof(fmt), "TargetPower(0.01dBm): %%d\r\nActualPower(0.01dBm): %%d\r\nRawPowerSetting: %%%us\r\n",
+             kRawPowerStringSize);
+    VerifyOrExit(sscanf(output, fmt, &targetPower, &actualPower, rawPowerSetting) == 3, error = OT_ERROR_FAILED);
+    SuccessOrExit(
+        error = ot::Utils::CmdLineParser::ParseAsHexString(rawPowerSetting, *aRawPowerSettingLength, aRawPowerSetting));
+    *aTargetPower = static_cast<int16_t>(targetPower);
+    *aActualPower = static_cast<int16_t>(actualPower);
+
+exit:
+    return error;
+}
+
+otError otPlatDiagRadioSetRawPowerSetting(otInstance    *aInstance,
+                                          const uint8_t *aRawPowerSetting,
+                                          uint16_t       aRawPowerSettingLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    otError error;
+    char    cmd[OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE];
+    int     nbytes;
+
+    assert(aRawPowerSetting != nullptr);
+
+    nbytes = snprintf(cmd, sizeof(cmd), "rawpowersetting ");
+
+    for (uint16_t i = 0; i < aRawPowerSettingLength; i++)
+    {
+        nbytes += snprintf(cmd + nbytes, sizeof(cmd) - static_cast<size_t>(nbytes), "%02x", aRawPowerSetting[i]);
+        VerifyOrExit(nbytes < static_cast<int>(sizeof(cmd)), error = OT_ERROR_INVALID_ARGS);
+    }
+
+    SuccessOrExit(error = sRadioSpinel.PlatDiagProcess(cmd, nullptr, 0));
+
+exit:
+    return error;
+}
+
+otError otPlatDiagRadioGetRawPowerSetting(otInstance *aInstance,
+                                          uint8_t    *aRawPowerSetting,
+                                          uint16_t   *aRawPowerSettingLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    otError error;
+    char    cmd[OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE];
+    char    output[OPENTHREAD_CONFIG_DIAG_OUTPUT_BUFFER_SIZE];
+    char   *str;
+
+    assert((aRawPowerSetting != nullptr) && (aRawPowerSettingLength != nullptr));
+
+    snprintf(cmd, sizeof(cmd), "rawpowersetting");
+    SuccessOrExit(error = sRadioSpinel.PlatDiagProcess(cmd, output, sizeof(output)));
+    VerifyOrExit((str = strtok(output, "\r")) != nullptr, error = OT_ERROR_FAILED);
+    SuccessOrExit(error = ot::Utils::CmdLineParser::ParseAsHexString(str, *aRawPowerSettingLength, aRawPowerSetting));
+
+exit:
+    return error;
+}
+
+otError otPlatDiagRadioRawPowerSettingEnable(otInstance *aInstance, bool aEnable)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    otError error;
+    char    cmd[OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE];
+
+    snprintf(cmd, sizeof(cmd), "rawpowersetting %s", aEnable ? "enable" : "disable");
+    SuccessOrExit(error = sRadioSpinel.PlatDiagProcess(cmd, nullptr, 0));
+
+exit:
+    return error;
+}
+
+otError otPlatDiagRadioTransmitCarrier(otInstance *aInstance, bool aEnable)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    otError error;
+    char    cmd[OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE];
+
+    snprintf(cmd, sizeof(cmd), "cw %s", aEnable ? "start" : "stop");
+    SuccessOrExit(error = sRadioSpinel.PlatDiagProcess(cmd, nullptr, 0));
+
+exit:
+    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);
@@ -554,10 +750,7 @@
     OT_UNUSED_VARIABLE(aError);
 }
 
-void otPlatDiagAlarmCallback(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
+void otPlatDiagAlarmCallback(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
 #endif // OPENTHREAD_CONFIG_DIAG_ENABLE
 
 uint32_t otPlatRadioGetSupportedChannelMask(otInstance *aInstance)
@@ -578,7 +771,7 @@
     return sRadioSpinel.GetState();
 }
 
-void otPlatRadioSetMacKey(otInstance *            aInstance,
+void otPlatRadioSetMacKey(otInstance             *aInstance,
                           uint8_t                 aKeyIdMode,
                           uint8_t                 aKeyId,
                           const otMacKeyMaterial *aPrevKey,
@@ -593,7 +786,13 @@
 
 void otPlatRadioSetMacFrameCounter(otInstance *aInstance, uint32_t aMacFrameCounter)
 {
-    SuccessOrDie(sRadioSpinel.SetMacFrameCounter(aMacFrameCounter));
+    SuccessOrDie(sRadioSpinel.SetMacFrameCounter(aMacFrameCounter, /* aSetIfLarger */ false));
+    OT_UNUSED_VARIABLE(aInstance);
+}
+
+void otPlatRadioSetMacFrameCounterIfLarger(otInstance *aInstance, uint32_t aMacFrameCounter)
+{
+    SuccessOrDie(sRadioSpinel.SetMacFrameCounter(aMacFrameCounter, /* aSetIfLarger */ true));
     OT_UNUSED_VARIABLE(aInstance);
 }
 
@@ -633,23 +832,56 @@
     return sRadioSpinel.SetChannelMaxTransmitPower(aChannel, aMaxPower);
 }
 
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+otError otPlatRadioAddCalibratedPower(otInstance    *aInstance,
+                                      uint8_t        aChannel,
+                                      int16_t        aActualPower,
+                                      const uint8_t *aRawPowerSetting,
+                                      uint16_t       aRawPowerSettingLength)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    return sRadioSpinel.AddCalibratedPower(aChannel, aActualPower, aRawPowerSetting, aRawPowerSettingLength);
+}
+
+otError otPlatRadioClearCalibratedPowers(otInstance *aInstance)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    return sRadioSpinel.ClearCalibratedPowers();
+}
+
+otError otPlatRadioSetChannelTargetPower(otInstance *aInstance, uint8_t aChannel, int16_t aTargetPower)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    return sRadioSpinel.SetChannelTargetPower(aChannel, aTargetPower);
+}
+#endif
+
 otError otPlatRadioSetRegion(otInstance *aInstance, uint16_t aRegionCode)
 {
     OT_UNUSED_VARIABLE(aInstance);
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+    return sPowerUpdater.SetRegion(aRegionCode);
+#else
     return sRadioSpinel.SetRadioRegion(aRegionCode);
+#endif
 }
 
 otError otPlatRadioGetRegion(otInstance *aInstance, uint16_t *aRegionCode)
 {
     OT_UNUSED_VARIABLE(aInstance);
+#if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+    *aRegionCode = sPowerUpdater.GetRegion();
+    return OT_ERROR_NONE;
+#else
     return sRadioSpinel.GetRadioRegion(aRegionCode);
+#endif
 }
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-otError otPlatRadioConfigureEnhAckProbing(otInstance *         aInstance,
+otError otPlatRadioConfigureEnhAckProbing(otInstance          *aInstance,
                                           otLinkMetrics        aLinkMetrics,
                                           const otShortAddress aShortAddress,
-                                          const otExtAddress * aExtAddress)
+                                          const otExtAddress  *aExtAddress)
 {
     OT_UNUSED_VARIABLE(aInstance);
 
@@ -666,10 +898,7 @@
     return OT_ERROR_NOT_IMPLEMENTED;
 }
 
-const otRadioSpinelMetrics *otSysGetRadioSpinelMetrics(void)
-{
-    return sRadioSpinel.GetRadioSpinelMetrics();
-}
+const otRadioSpinelMetrics *otSysGetRadioSpinelMetrics(void) { return sRadioSpinel.GetRadioSpinelMetrics(); }
 
 const otRcpInterfaceMetrics *otSysGetRcpInterfaceMetrics(void)
 {
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
new file mode 100644
index 0000000..c8d80e2
--- /dev/null
+++ b/src/posix/platform/resolver.cpp
@@ -0,0 +1,309 @@
+/*
+ *  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 "resolver.hpp"
+
+#include "platform-posix.h"
+
+#include <openthread/logging.h>
+#include <openthread/message.h>
+#include <openthread/udp.h>
+#include <openthread/platform/dns.h>
+#include <openthread/platform/time.h>
+
+#include "common/code_utils.hpp"
+
+#include <arpa/inet.h>
+#include <arpa/nameser.h>
+#include <cassert>
+#include <netinet/in.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <fstream>
+#include <string>
+
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+
+namespace {
+constexpr char kResolvConfFullPath[] = "/etc/resolv.conf";
+constexpr char kNameserverItem[]     = "nameserver";
+} // namespace
+
+extern ot::Posix::Resolver gResolver;
+
+namespace ot {
+namespace Posix {
+
+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;
+
+    mUpstreamDnsServerCount = 0;
+
+    fp.open(kResolvConfFullPath);
+
+    while (fp.good() && std::getline(fp, line) && mUpstreamDnsServerCount < kMaxUpstreamServerCount)
+    {
+        if (line.find(kNameserverItem, 0) == 0)
+        {
+            in_addr_t addr;
+
+            if (inet_pton(AF_INET, &line.c_str()[sizeof(kNameserverItem)], &addr) == 1)
+            {
+                otLogInfoPlat("Got nameserver #%d: %s", mUpstreamDnsServerCount,
+                              &line.c_str()[sizeof(kNameserverItem)]);
+                mUpstreamDnsServerList[mUpstreamDnsServerCount] = addr;
+                mUpstreamDnsServerCount++;
+            }
+        }
+    }
+
+    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)
+{
+    char        packet[kMaxDnsMessageSize];
+    otError     error  = OT_ERROR_NONE;
+    uint16_t    length = otMessageGetLength(aQuery);
+    sockaddr_in serverAddr;
+
+    Transaction *txn = nullptr;
+
+    VerifyOrExit(length <= kMaxDnsMessageSize, error = OT_ERROR_NO_BUFS);
+    VerifyOrExit(otMessageRead(aQuery, 0, &packet, sizeof(packet)) == length, error = OT_ERROR_NO_BUFS);
+
+    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++)
+    {
+        serverAddr.sin_addr.s_addr = mUpstreamDnsServerList[i];
+        VerifyOrExit(
+            sendto(txn->mUdpFd, packet, length, MSG_DONTWAIT, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) > 0,
+            error = OT_ERROR_NO_ROUTE);
+    }
+    otLogInfoPlat("Forwarded DNS query %p to %d server(s).", static_cast<void *>(aTxn), mUpstreamDnsServerCount);
+
+exit:
+    if (error != OT_ERROR_NONE)
+    {
+        otLogCritPlat("Failed to forward DNS query %p to server: %d", static_cast<void *>(aTxn), error);
+    }
+    return;
+}
+
+void Resolver::Cancel(otPlatDnsUpstreamQuery *aTxn)
+{
+    Transaction *txn = GetTransaction(aTxn);
+
+    if (txn != nullptr)
+    {
+        CloseTransaction(txn);
+    }
+
+    otPlatDnsUpstreamQueryDone(gInstance, aTxn, nullptr);
+}
+
+Resolver::Transaction *Resolver::AllocateTransaction(otPlatDnsUpstreamQuery *aThreadTxn)
+{
+    int          fdOrError = 0;
+    Transaction *ret       = nullptr;
+
+    for (Transaction &txn : mUpstreamTransaction)
+    {
+        if (txn.mThreadTxn == nullptr)
+        {
+            fdOrError = socket(AF_INET, SOCK_DGRAM, 0);
+            if (fdOrError < 0)
+            {
+                otLogInfoPlat("Failed to create socket for upstream resolver: %d", fdOrError);
+                break;
+            }
+            ret             = &txn;
+            ret->mUdpFd     = fdOrError;
+            ret->mThreadTxn = aThreadTxn;
+            break;
+        }
+    }
+
+    return ret;
+}
+
+void Resolver::ForwardResponse(Transaction *aTxn)
+{
+    char       response[kMaxDnsMessageSize];
+    ssize_t    readSize;
+    otError    error   = OT_ERROR_NONE;
+    otMessage *message = nullptr;
+
+    VerifyOrExit((readSize = read(aTxn->mUdpFd, response, sizeof(response))) > 0);
+
+    message = otUdpNewMessage(gInstance, nullptr);
+    VerifyOrExit(message != nullptr, error = OT_ERROR_NO_BUFS);
+    SuccessOrExit(error = otMessageAppend(message, response, readSize));
+
+    otPlatDnsUpstreamQueryDone(gInstance, aTxn->mThreadTxn, message);
+    message = nullptr;
+
+exit:
+    if (readSize < 0)
+    {
+        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);
+    }
+}
+
+Resolver::Transaction *Resolver::GetTransaction(int aFd)
+{
+    Transaction *ret = nullptr;
+
+    for (Transaction &txn : mUpstreamTransaction)
+    {
+        if (txn.mThreadTxn != nullptr && txn.mUdpFd == aFd)
+        {
+            ret = &txn;
+            break;
+        }
+    }
+
+    return ret;
+}
+
+Resolver::Transaction *Resolver::GetTransaction(otPlatDnsUpstreamQuery *aThreadTxn)
+{
+    Transaction *ret = nullptr;
+
+    for (Transaction &txn : mUpstreamTransaction)
+    {
+        if (txn.mThreadTxn == aThreadTxn)
+        {
+            ret = &txn;
+            break;
+        }
+    }
+
+    return ret;
+}
+
+void Resolver::CloseTransaction(Transaction *aTxn)
+{
+    if (aTxn->mUdpFd >= 0)
+    {
+        close(aTxn->mUdpFd);
+        aTxn->mUdpFd = -1;
+    }
+    aTxn->mThreadTxn = nullptr;
+}
+
+void Resolver::UpdateFdSet(otSysMainloopContext &aContext)
+{
+    for (Transaction &txn : mUpstreamTransaction)
+    {
+        if (txn.mThreadTxn != nullptr)
+        {
+            FD_SET(txn.mUdpFd, &aContext.mReadFdSet);
+            FD_SET(txn.mUdpFd, &aContext.mErrorFdSet);
+            if (txn.mUdpFd > aContext.mMaxFd)
+            {
+                aContext.mMaxFd = txn.mUdpFd;
+            }
+        }
+    }
+}
+
+void Resolver::Process(const otSysMainloopContext &aContext)
+{
+    for (Transaction &txn : mUpstreamTransaction)
+    {
+        if (txn.mThreadTxn != nullptr)
+        {
+            // Note: On Linux, we can only get the error via read, so they should share the same logic.
+            if (FD_ISSET(txn.mUdpFd, &aContext.mErrorFdSet) || FD_ISSET(txn.mUdpFd, &aContext.mReadFdSet))
+            {
+                ForwardResponse(&txn);
+                CloseTransaction(&txn);
+            }
+        }
+    }
+}
+
+} // namespace Posix
+} // namespace ot
+
+void otPlatDnsStartUpstreamQuery(otInstance *aInstance, otPlatDnsUpstreamQuery *aTxn, const otMessage *aQuery)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    gResolver.Query(aTxn, aQuery);
+}
+
+void otPlatDnsCancelUpstreamQuery(otInstance *aInstance, otPlatDnsUpstreamQuery *aTxn)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    gResolver.Cancel(aTxn);
+}
+
+#endif // OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
diff --git a/src/posix/platform/resolver.hpp b/src/posix/platform/resolver.hpp
new file mode 100644
index 0000000..4c50b08
--- /dev/null
+++ b/src/posix/platform/resolver.hpp
@@ -0,0 +1,125 @@
+/*
+ *  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.
+ */
+
+#ifndef POSIX_PLATFORM_RESOLVER_HPP_
+#define POSIX_PLATFORM_RESOLVER_HPP_
+
+#include <openthread/openthread-system.h>
+#include <openthread/platform/dns.h>
+
+#include <arpa/inet.h>
+#include <sys/select.h>
+
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+
+namespace ot {
+namespace Posix {
+
+class Resolver
+{
+public:
+    constexpr static ssize_t kMaxDnsMessageSize           = 512;
+    constexpr static ssize_t kMaxUpstreamTransactionCount = 16;
+    constexpr static ssize_t kMaxUpstreamServerCount      = 3;
+
+    /**
+     * This method initialize the upstream DNS resolver.
+     *
+     */
+    void Init(void);
+
+    /**
+     * Sends the query to the upstream.
+     *
+     * @param[in] aTxn   A pointer to the OpenThread upstream DNS query transaction.
+     * @param[in] aQuery A pointer to a message for the payload of the DNS query.
+     *
+     */
+    void Query(otPlatDnsUpstreamQuery *aTxn, const otMessage *aQuery);
+
+    /**
+     * Cancels a upstream DNS query transaction.
+     *
+     * @param[in] aTxn   A pointer to the OpenThread upstream DNS query transaction.
+     *
+     */
+    void Cancel(otPlatDnsUpstreamQuery *aTxn);
+
+    /**
+     * Updates the file descriptor sets with file descriptors used by the radio driver.
+     *
+     * @param[in,out]  aReadFdSet   A reference to the read file descriptors.
+     * @param[in,out]  aErrorFdSet  A reference to the error file descriptors.
+     * @param[in,out]  aMaxFd       A reference to the max file descriptor.
+     * @param[in,out]  aTimeout     A reference to the timeout.
+     *
+     */
+    void UpdateFdSet(otSysMainloopContext &aContext);
+
+    /**
+     * Handles the result of select.
+     *
+     * @param[in]  aReadFdSet   A reference to the read file descriptors.
+     * @param[in]  aErrorFdSet  A reference to the error file descriptors.
+     *
+     */
+    void Process(const otSysMainloopContext &aContext);
+
+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;
+        int                     mUdpFd;
+    };
+
+    Transaction *GetTransaction(int aFd);
+    Transaction *GetTransaction(otPlatDnsUpstreamQuery *aThreadTxn);
+    Transaction *AllocateTransaction(otPlatDnsUpstreamQuery *aThreadTxn);
+
+    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];
+};
+
+} // namespace Posix
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+
+#endif // POSIX_PLATFORM_RESOLVER_HPP_
diff --git a/src/posix/platform/settings.cpp b/src/posix/platform/settings.cpp
index 7ea03ab..f807ebc 100644
--- a/src/posix/platform/settings.cpp
+++ b/src/posix/platform/settings.cpp
@@ -525,10 +525,7 @@
 
 #if SELF_TEST
 
-void otLogCritPlat(const char *aFormat, ...)
-{
-    OT_UNUSED_VARIABLE(aFormat);
-}
+void otLogCritPlat(const char *aFormat, ...) { OT_UNUSED_VARIABLE(aFormat); }
 
 const char *otExitCodeToString(uint8_t aExitCode)
 {
@@ -544,10 +541,7 @@
 }
 
 // Stub implementation for testing
-bool IsSystemDryRun(void)
-{
-    return false;
-}
+bool IsSystemDryRun(void) { return false; }
 
 int main()
 {
diff --git a/src/posix/platform/spi_interface.cpp b/src/posix/platform/spi_interface.cpp
index 33f6d0c..ccf366e 100644
--- a/src/posix/platform/spi_interface.cpp
+++ b/src/posix/platform/spi_interface.cpp
@@ -65,8 +65,8 @@
 namespace Posix {
 
 SpiInterface::SpiInterface(SpinelInterface::ReceiveFrameCallback aCallback,
-                           void *                                aCallbackContext,
-                           SpinelInterface::RxFrameBuffer &      aFrameBuffer)
+                           void                                 *aCallbackContext,
+                           SpinelInterface::RxFrameBuffer       &aFrameBuffer)
     : mReceiveFrameCallback(aCallback)
     , mReceiveFrameContext(aCallbackContext)
     , mRxFrameBuffer(aFrameBuffer)
@@ -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 outputing invalid
+    // log messages.
     usleep(static_cast<useconds_t>(mSpiResetDelay) * kUsecPerMsec);
+
+    return OT_ERROR_NONE;
 }
 
 otError SpiInterface::Init(const Url::Url &aRadioUrl)
@@ -182,19 +193,10 @@
     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;
 }
 
-SpiInterface::~SpiInterface(void)
-{
-    Deinit();
-}
+SpiInterface::~SpiInterface(void) { Deinit(); }
 
 void SpiInterface::Deinit(void)
 {
@@ -343,7 +345,7 @@
 
 uint8_t *SpiInterface::GetRealRxFrameStart(uint8_t *aSpiRxFrameBuffer, uint8_t aAlignAllowance, uint16_t &aSkipLength)
 {
-    uint8_t *      start = aSpiRxFrameBuffer;
+    uint8_t       *start = aSpiRxFrameBuffer;
     const uint8_t *end   = aSpiRxFrameBuffer + aAlignAllowance;
 
     for (; start != end && start[0] == 0xff; start++)
@@ -408,8 +410,8 @@
     uint16_t      spiTransferBytes    = 0;
     uint8_t       successfulExchanges = 0;
     bool          discardRxFrame      = true;
-    uint8_t *     spiRxFrameBuffer;
-    uint8_t *     spiRxFrame;
+    uint8_t      *spiRxFrameBuffer;
+    uint8_t      *spiRxFrame;
     uint8_t       slaveHeader;
     uint16_t      slaveAcceptLen;
     Ncp::SpiFrame txFrame(mSpiTxFrameBuffer);
@@ -627,12 +629,13 @@
     return (mIntGpioValueFd >= 0) ? (GetGpioValue(mIntGpioValueFd) == kGpioIntAssertState) : true;
 }
 
-void SpiInterface::UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, int &aMaxFd, struct timeval &aTimeout)
+void SpiInterface::UpdateFdSet(void *aMainloopContext)
 {
-    struct timeval timeout        = {kSecPerDay, 0};
-    struct timeval pollingTimeout = {0, kSpiPollPeriodUs};
+    struct timeval        timeout        = {kSecPerDay, 0};
+    struct timeval        pollingTimeout = {0, kSpiPollPeriodUs};
+    otSysMainloopContext *context        = reinterpret_cast<otSysMainloopContext *>(aMainloopContext);
 
-    OT_UNUSED_VARIABLE(aWriteFdSet);
+    assert(context != nullptr);
 
     if (mSpiTxIsReady)
     {
@@ -643,9 +646,9 @@
 
     if (mIntGpioValueFd >= 0)
     {
-        if (aMaxFd < mIntGpioValueFd)
+        if (context->mMaxFd < mIntGpioValueFd)
         {
-            aMaxFd = mIntGpioValueFd;
+            context->mMaxFd = mIntGpioValueFd;
         }
 
         if (CheckInterrupt())
@@ -659,7 +662,7 @@
         {
             // The interrupt pin was not asserted, so we wait for the interrupt pin to be asserted by adding it to the
             // read set.
-            FD_SET(mIntGpioValueFd, &aReadFdSet);
+            FD_SET(mIntGpioValueFd, &context->mReadFdSet);
         }
     }
     else if (timercmp(&pollingTimeout, &timeout, <))
@@ -719,22 +722,19 @@
         mDidPrintRateLimitLog = false;
     }
 
-    if (timercmp(&timeout, &aTimeout, <))
+    if (timercmp(&timeout, &context->mTimeout, <))
     {
-        aTimeout = timeout;
+        context->mTimeout = timeout;
     }
 }
 
-void SpiInterface::Process(const RadioProcessContext &aContext)
+void SpiInterface::Process(const void *aMainloopContext)
 {
-    Process(aContext.mReadFdSet, aContext.mWriteFdSet);
-}
+    const otSysMainloopContext *context = reinterpret_cast<const otSysMainloopContext *>(aMainloopContext);
 
-void SpiInterface::Process(const fd_set *aReadFdSet, const fd_set *aWriteFdSet)
-{
-    OT_UNUSED_VARIABLE(aWriteFdSet);
+    assert(context != nullptr);
 
-    if (FD_ISSET(mIntGpioValueFd, aReadFdSet))
+    if (FD_ISSET(mIntGpioValueFd, &context->mReadFdSet))
     {
         struct gpioevent_data event;
 
@@ -762,25 +762,23 @@
 
     while (now < end)
     {
-        fd_set         readFdSet;
-        fd_set         writeFdSet;
-        int            maxFds = -1;
-        struct timeval timeout;
-        int            ret;
+        otSysMainloopContext context;
+        int                  ret;
 
-        timeout.tv_sec  = static_cast<time_t>((end - now) / US_PER_S);
-        timeout.tv_usec = static_cast<suseconds_t>((end - now) % US_PER_S);
+        context.mMaxFd           = -1;
+        context.mTimeout.tv_sec  = static_cast<time_t>((end - now) / US_PER_S);
+        context.mTimeout.tv_usec = static_cast<suseconds_t>((end - now) % US_PER_S);
 
-        FD_ZERO(&readFdSet);
-        FD_ZERO(&writeFdSet);
+        FD_ZERO(&context.mReadFdSet);
+        FD_ZERO(&context.mWriteFdSet);
 
-        UpdateFdSet(readFdSet, writeFdSet, maxFds, timeout);
+        UpdateFdSet(&context);
 
-        ret = select(maxFds + 1, &readFdSet, &writeFdSet, nullptr, &timeout);
+        ret = select(context.mMaxFd + 1, &context.mReadFdSet, &context.mWriteFdSet, nullptr, &context.mTimeout);
 
         if (ret >= 0)
         {
-            Process(&readFdSet, &writeFdSet);
+            Process(&context);
 
             if (mDidRxFrame)
             {
@@ -806,6 +804,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 b2e8a02..549b0c4 100644
--- a/src/posix/platform/spi_interface.hpp
+++ b/src/posix/platform/spi_interface.hpp
@@ -42,8 +42,6 @@
 
 #include <openthread/openthread-system.h>
 
-#if OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_SPI
-
 #include "ncp/ncp_spi.hpp"
 
 namespace ot {
@@ -53,7 +51,7 @@
  * This class defines an SPI interface to the Radio Co-processor (RCP).
  *
  */
-class SpiInterface
+class SpiInterface : public ot::Spinel::SpinelInterface
 {
 public:
     /**
@@ -65,8 +63,8 @@
      *
      */
     SpiInterface(Spinel::SpinelInterface::ReceiveFrameCallback aCallback,
-                 void *                                        aCallbackContext,
-                 Spinel::SpinelInterface::RxFrameBuffer &      aFrameBuffer);
+                 void                                         *aCallbackContext,
+                 Spinel::SpinelInterface::RxFrameBuffer       &aFrameBuffer);
 
     /**
      * This destructor deinitializes the object.
@@ -122,21 +120,18 @@
     /**
      * This method updates the file descriptor sets with file descriptors used by the radio driver.
      *
-     * @param[in,out]  aReadFdSet   A reference to the read file descriptors.
-     * @param[in,out]  aWriteFdSet  A reference to the write file descriptors.
-     * @param[in,out]  aMaxFd       A reference to the max file descriptor.
-     * @param[in,out]  aTimeout     A reference to the timeout.
+     * @param[in,out]   aMainloopContext  A pointer to the mainloop context containing fd_sets.
      *
      */
-    void UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, int &aMaxFd, struct timeval &aTimeout);
+    void UpdateFdSet(void *aMainloopContext);
 
     /**
      * This method performs radio driver processing.
      *
-     * @param[in]   aContext        The context containing fd_sets.
+     * @param[in]   aMainloopContext  A pointer to the mainloop context containing fd_sets.
      *
      */
-    void Process(const RadioProcessContext &aContext);
+    void Process(const void *aMainloopContext);
 
     /**
      * This method returns the bus speed between the host and the radio.
@@ -147,17 +142,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 +159,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);
@@ -181,7 +173,6 @@
     uint8_t *GetRealRxFrameStart(uint8_t *aSpiRxFrameBuffer, uint8_t aAlignAllowance, uint16_t &aSkipLength);
     otError  DoSpiTransfer(uint8_t *aSpiRxFrameBuffer, uint32_t aTransferLength);
     otError  PushPullSpi(void);
-    void     Process(const fd_set *aReadFdSet, const fd_set *aWriteFdSet);
 
     bool CheckInterrupt(void);
     void LogStats(void);
@@ -221,8 +212,8 @@
     };
 
     Spinel::SpinelInterface::ReceiveFrameCallback mReceiveFrameCallback;
-    void *                                        mReceiveFrameContext;
-    Spinel::SpinelInterface::RxFrameBuffer &      mRxFrameBuffer;
+    void                                         *mReceiveFrameContext;
+    Spinel::SpinelInterface::RxFrameBuffer       &mRxFrameBuffer;
 
     int mSpiDevFd;
     int mResetGpioValueFd;
@@ -259,5 +250,4 @@
 } // namespace Posix
 } // namespace ot
 
-#endif // OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_SPI
 #endif // POSIX_APP_SPI_INTERFACE_HPP_
diff --git a/src/posix/platform/system.cpp b/src/posix/platform/system.cpp
index c3e176a..3438839 100644
--- a/src/posix/platform/system.cpp
+++ b/src/posix/platform/system.cpp
@@ -36,6 +36,7 @@
 #include "platform-posix.h"
 
 #include <assert.h>
+#include <inttypes.h>
 
 #include <openthread-core-config.h>
 #include <openthread/border_router.h>
@@ -122,6 +123,10 @@
 
 void platformInit(otPlatformConfig *aPlatformConfig)
 {
+#if OPENTHREAD_POSIX_CONFIG_BACKTRACE_ENABLE
+    platformBacktraceInit();
+#endif
+
     platformAlarmInit(aPlatformConfig->mSpeedUpFactor, aPlatformConfig->mRealTimeSignal);
     platformRadioInit(get802154RadioUrl(aPlatformConfig));
 
@@ -137,14 +142,21 @@
     platformBackboneInit(aPlatformConfig->mBackboneInterfaceName);
 #endif
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#if OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
     ot::Posix::InfraNetif::Get().Init(aPlatformConfig->mBackboneInterfaceName);
 #endif
 
     gNetifName[0] = '\0';
 
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    if (otIp4CidrFromString(OPENTHREAD_POSIX_CONFIG_NAT64_CIDR, &gNat64Cidr) != OT_ERROR_NONE)
+    {
+        gNat64Cidr.mLength = 0;
+    }
+#endif
+
 #if OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE
-    platformNetifInit(aPlatformConfig->mInterfaceName);
+    platformNetifInit(aPlatformConfig);
 #endif
 
 #if OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE
@@ -167,7 +179,7 @@
     platformBackboneSetUp();
 #endif
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#if OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
     ot::Posix::InfraNetif::Get().SetUp();
 #endif
 
@@ -222,7 +234,7 @@
     platformNetifTearDown();
 #endif
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#if OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
     ot::Posix::InfraNetif::Get().TearDown();
 #endif
 
@@ -254,7 +266,7 @@
     platformTrelDeinit();
 #endif
 
-#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+#if OPENTHREAD_POSIX_CONFIG_INFRA_IF_ENABLE
     ot::Posix::InfraNetif::Get().Deinit();
 #endif
 
@@ -280,29 +292,26 @@
 /**
  * This function try selecting the given file descriptors in nonblocking mode.
  *
- * @param[in,out]   aReadFdSet   A pointer to the read file descriptors.
- * @param[in,out]   aWriteFdSet  A pointer to the write file descriptors.
- * @param[in,out]   aErrorFdSet  A pointer to the error file descriptors.
- * @param[in]       aMaxFd       The max file descriptor.
+ * @param[in,out]  aContext  A reference to the mainloop context.
  *
  * @returns The value returned from select().
  *
  */
-static int trySelect(fd_set *aReadFdSet, fd_set *aWriteFdSet, fd_set *aErrorFdSet, int aMaxFd)
+static int trySelect(otSysMainloopContext &aContext)
 {
     struct timeval timeout          = {0, 0};
-    fd_set         originReadFdSet  = *aReadFdSet;
-    fd_set         originWriteFdSet = *aWriteFdSet;
-    fd_set         originErrorFdSet = *aErrorFdSet;
+    fd_set         originReadFdSet  = aContext.mReadFdSet;
+    fd_set         originWriteFdSet = aContext.mWriteFdSet;
+    fd_set         originErrorFdSet = aContext.mErrorFdSet;
     int            rval;
 
-    rval = select(aMaxFd + 1, aReadFdSet, aWriteFdSet, aErrorFdSet, &timeout);
+    rval = select(aContext.mMaxFd + 1, &aContext.mReadFdSet, &aContext.mWriteFdSet, &aContext.mErrorFdSet, &timeout);
 
     if (rval == 0)
     {
-        *aReadFdSet  = originReadFdSet;
-        *aWriteFdSet = originWriteFdSet;
-        *aErrorFdSet = originErrorFdSet;
+        aContext.mReadFdSet  = originReadFdSet;
+        aContext.mWriteFdSet = originWriteFdSet;
+        aContext.mErrorFdSet = originErrorFdSet;
     }
 
     return rval;
@@ -315,17 +324,15 @@
 
     platformAlarmUpdateTimeout(&aMainloop->mTimeout);
 #if OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE
-    platformNetifUpdateFdSet(&aMainloop->mReadFdSet, &aMainloop->mWriteFdSet, &aMainloop->mErrorFdSet,
-                             &aMainloop->mMaxFd);
+    platformNetifUpdateFdSet(aMainloop);
 #endif
 #if OPENTHREAD_POSIX_VIRTUAL_TIME
-    virtualTimeUpdateFdSet(&aMainloop->mReadFdSet, &aMainloop->mWriteFdSet, &aMainloop->mErrorFdSet, &aMainloop->mMaxFd,
-                           &aMainloop->mTimeout);
+    virtualTimeUpdateFdSet(aMainloop);
 #else
-    platformRadioUpdateFdSet(&aMainloop->mReadFdSet, &aMainloop->mWriteFdSet, &aMainloop->mMaxFd, &aMainloop->mTimeout);
+    platformRadioUpdateFdSet(aMainloop);
 #endif
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
-    platformTrelUpdateFdSet(&aMainloop->mReadFdSet, &aMainloop->mWriteFdSet, &aMainloop->mMaxFd, &aMainloop->mTimeout);
+    platformTrelUpdateFdSet(aMainloop);
 #endif
 
     if (otTaskletsArePending(aInstance))
@@ -343,7 +350,7 @@
     if (timerisset(&aMainloop->mTimeout))
     {
         // Make sure there are no data ready in UART
-        rval = trySelect(&aMainloop->mReadFdSet, &aMainloop->mWriteFdSet, &aMainloop->mErrorFdSet, aMainloop->mMaxFd);
+        rval = trySelect(*aMainloop);
 
         if (rval == 0)
         {
@@ -383,20 +390,17 @@
     ot::Posix::Mainloop::Manager::Get().Process(*aMainloop);
 
 #if OPENTHREAD_POSIX_VIRTUAL_TIME
-    virtualTimeProcess(aInstance, &aMainloop->mReadFdSet, &aMainloop->mWriteFdSet, &aMainloop->mErrorFdSet);
+    virtualTimeProcess(aInstance, aMainloop);
 #else
-    platformRadioProcess(aInstance, &aMainloop->mReadFdSet, &aMainloop->mWriteFdSet);
+    platformRadioProcess(aInstance, aMainloop);
 #endif
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
-    platformTrelProcess(aInstance, &aMainloop->mReadFdSet, &aMainloop->mWriteFdSet);
+    platformTrelProcess(aInstance, aMainloop);
 #endif
     platformAlarmProcess(aInstance);
 #if OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE
-    platformNetifProcess(&aMainloop->mReadFdSet, &aMainloop->mWriteFdSet, &aMainloop->mErrorFdSet);
+    platformNetifProcess(aMainloop);
 #endif
 }
 
-bool IsSystemDryRun(void)
-{
-    return gDryRun;
-}
+bool IsSystemDryRun(void) { return gDryRun; }
diff --git a/src/posix/platform/trel.cpp b/src/posix/platform/trel.cpp
index f3ec6cf..2ddab4b 100644
--- a/src/posix/platform/trel.cpp
+++ b/src/posix/platform/trel.cpp
@@ -85,8 +85,8 @@
     static char    string[1600];
 
     uint16_t num = 0;
-    char *   cur = &string[0];
-    char *   end = &string[sizeof(string) - 1];
+    char    *cur = &string[0];
+    char    *end = &string[sizeof(string) - 1];
 
     cur += snprintf(cur, (uint16_t)(end - cur), "[(len:%d) ", aLength);
     VerifyOrExit(cur < end);
@@ -388,24 +388,20 @@
     // advertising TREL service after this call.
 }
 
-OT_TOOL_WEAK void trelDnssdUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, int *aMaxFd, struct timeval *aTimeout)
+OT_TOOL_WEAK void trelDnssdUpdateFdSet(otSysMainloopContext *aContext)
 {
     // This function can be used to update the file descriptor sets
     // by DNS-SD layer (if needed).
 
-    OT_UNUSED_VARIABLE(aReadFdSet);
-    OT_UNUSED_VARIABLE(aWriteFdSet);
-    OT_UNUSED_VARIABLE(aMaxFd);
-    OT_UNUSED_VARIABLE(aTimeout);
+    OT_UNUSED_VARIABLE(aContext);
 }
 
-OT_TOOL_WEAK void trelDnssdProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet)
+OT_TOOL_WEAK void trelDnssdProcess(otInstance *aInstance, const otSysMainloopContext *aContext)
 {
     // This function performs processing by DNS-SD (if needed).
 
     OT_UNUSED_VARIABLE(aInstance);
-    OT_UNUSED_VARIABLE(aReadFdSet);
-    OT_UNUSED_VARIABLE(aWriteFdSet);
+    OT_UNUSED_VARIABLE(aContext);
 }
 
 //---------------------------------------------------------------------------------------------------------------------
@@ -449,8 +445,8 @@
     return;
 }
 
-void otPlatTrelSend(otInstance *      aInstance,
-                    const uint8_t *   aUdpPayload,
+void otPlatTrelSend(otInstance       *aInstance,
+                    const uint8_t    *aUdpPayload,
                     uint16_t          aUdpPayloadLen,
                     const otSockAddr *aDestSockAddr)
 {
@@ -525,45 +521,45 @@
     return;
 }
 
-void platformTrelUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, int *aMaxFd, struct timeval *aTimeout)
+void platformTrelUpdateFdSet(otSysMainloopContext *aContext)
 {
-    assert((aReadFdSet != NULL) && (aWriteFdSet != NULL) && (aMaxFd != NULL) && (aTimeout != NULL));
+    assert(aContext != nullptr);
 
     VerifyOrExit(sEnabled);
 
-    FD_SET(sSocket, aReadFdSet);
+    FD_SET(sSocket, &aContext->mReadFdSet);
 
-    if (sTxPacketQueueTail != NULL)
+    if (sTxPacketQueueTail != nullptr)
     {
-        FD_SET(sSocket, aWriteFdSet);
+        FD_SET(sSocket, &aContext->mWriteFdSet);
     }
 
-    if (*aMaxFd < sSocket)
+    if (aContext->mMaxFd < sSocket)
     {
-        *aMaxFd = sSocket;
+        aContext->mMaxFd = sSocket;
     }
 
-    trelDnssdUpdateFdSet(aReadFdSet, aWriteFdSet, aMaxFd, aTimeout);
+    trelDnssdUpdateFdSet(aContext);
 
 exit:
     return;
 }
 
-void platformTrelProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet)
+void platformTrelProcess(otInstance *aInstance, const otSysMainloopContext *aContext)
 {
     VerifyOrExit(sEnabled);
 
-    if (FD_ISSET(sSocket, aWriteFdSet))
+    if (FD_ISSET(sSocket, &aContext->mWriteFdSet))
     {
         SendQueuedPackets();
     }
 
-    if (FD_ISSET(sSocket, aReadFdSet))
+    if (FD_ISSET(sSocket, &aContext->mReadFdSet))
     {
         ReceivePacket(sSocket, aInstance);
     }
 
-    trelDnssdProcess(aInstance, aReadFdSet, aWriteFdSet);
+    trelDnssdProcess(aInstance, aContext);
 
 exit:
     return;
diff --git a/src/posix/platform/udp.cpp b/src/posix/platform/udp.cpp
index b7aacc5..7f78acc 100644
--- a/src/posix/platform/udp.cpp
+++ b/src/posix/platform/udp.cpp
@@ -65,25 +65,13 @@
 
 constexpr size_t kMaxUdpSize = 1280;
 
-void *FdToHandle(int aFd)
-{
-    return reinterpret_cast<void *>(aFd);
-}
+void *FdToHandle(int aFd) { return reinterpret_cast<void *>(aFd); }
 
-int FdFromHandle(void *aHandle)
-{
-    return static_cast<int>(reinterpret_cast<long>(aHandle));
-}
+int FdFromHandle(void *aHandle) { return static_cast<int>(reinterpret_cast<long>(aHandle)); }
 
-bool IsLinkLocal(const struct in6_addr &aAddress)
-{
-    return aAddress.s6_addr[0] == 0xfe && aAddress.s6_addr[1] == 0x80;
-}
+bool IsLinkLocal(const struct in6_addr &aAddress) { return aAddress.s6_addr[0] == 0xfe && aAddress.s6_addr[1] == 0x80; }
 
-bool IsMulticast(const otIp6Address &aAddress)
-{
-    return aAddress.mFields.m8[0] == 0xff;
-}
+bool IsMulticast(const otIp6Address &aAddress) { return aAddress.mFields.m8[0] == 0xff; }
 
 otError transmitPacket(int aFd, uint8_t *aPayload, uint16_t aLength, const otMessageInfo &aMessageInfo)
 {
@@ -98,7 +86,7 @@
     size_t              controlLength = 0;
     struct iovec        iov;
     struct msghdr       msg;
-    struct cmsghdr *    cmsg;
+    struct cmsghdr     *cmsg;
     ssize_t             rval;
     otError             error = OT_ERROR_NONE;
 
@@ -317,7 +305,7 @@
 #else  // __NetBSD__ || __FreeBSD__ || __APPLE__
         unsigned int netifIndex = 0;
         VerifyOrExit(setsockopt(fd, IPPROTO_IPV6, IPV6_BOUND_IF, &netifIndex, sizeof(netifIndex)) == 0,
-                     error = OT_ERROR_FAILED);
+                               error = OT_ERROR_FAILED);
 #endif // __linux__
         break;
     }
@@ -328,7 +316,7 @@
                      error = OT_ERROR_FAILED);
 #else  // __NetBSD__ || __FreeBSD__ || __APPLE__
         VerifyOrExit(setsockopt(fd, IPPROTO_IPV6, IPV6_BOUND_IF, &gNetifIndex, sizeof(gNetifIndex)) == 0,
-                     error = OT_ERROR_FAILED);
+                               error = OT_ERROR_FAILED);
 #endif // __linux__
         break;
     }
@@ -388,8 +376,8 @@
 
         if (getsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &netifName, &len) != 0)
         {
-            otLogWarnPlat("Failed to read socket bound device: %s", strerror(errno));
-            len = 0;
+                      otLogWarnPlat("Failed to read socket bound device: %s", strerror(errno));
+                      len = 0;
         }
 
         // There is a bug in linux that connecting to AF_UNSPEC does not disconnect.
@@ -400,11 +388,11 @@
 
         if (len > 0 && netifName[0] != '\0')
         {
-            fd = FdFromHandle(aUdpSocket->mHandle);
-            VerifyOrExit(setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &netifName, len) == 0, {
-                otLogWarnPlat("Failed to bind to device: %s", strerror(errno));
-                error = OT_ERROR_FAILED;
-            });
+                      fd = FdFromHandle(aUdpSocket->mHandle);
+                      VerifyOrExit(setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &netifName, len) == 0, {
+                          otLogWarnPlat("Failed to bind to device: %s", strerror(errno));
+                          error = OT_ERROR_FAILED;
+                      });
         }
 
         ExitNow();
@@ -463,7 +451,7 @@
     return error;
 }
 
-otError otPlatUdpJoinMulticastGroup(otUdpSocket *       aUdpSocket,
+otError otPlatUdpJoinMulticastGroup(otUdpSocket        *aUdpSocket,
                                     otNetifIdentifier   aNetifIdentifier,
                                     const otIp6Address *aAddress)
 {
@@ -503,7 +491,7 @@
     return error;
 }
 
-otError otPlatUdpLeaveMulticastGroup(otUdpSocket *       aUdpSocket,
+otError otPlatUdpLeaveMulticastGroup(otUdpSocket        *aUdpSocket,
                                      otNetifIdentifier   aNetifIdentifier,
                                      const otIp6Address *aAddress)
 {
@@ -591,15 +579,9 @@
     assert(gNetifIndex != 0);
 }
 
-void Udp::SetUp(void)
-{
-    Mainloop::Manager::Get().Add(*this);
-}
+void Udp::SetUp(void) { Mainloop::Manager::Get().Add(*this); }
 
-void Udp::TearDown(void)
-{
-    Mainloop::Manager::Get().Remove(*this);
-}
+void Udp::TearDown(void) { Mainloop::Manager::Get().Remove(*this); }
 
 void Udp::Deinit(void)
 {
@@ -624,7 +606,7 @@
         if (fd > 0 && FD_ISSET(fd, &aContext.mReadFdSet))
         {
             otMessageInfo messageInfo;
-            otMessage *   message = nullptr;
+            otMessage    *message = nullptr;
             uint8_t       payload[kMaxUdpSize];
             uint16_t      length = sizeof(payload);
 
diff --git a/src/posix/platform/utils.cpp b/src/posix/platform/utils.cpp
index e497404..8183ac3 100644
--- a/src/posix/platform/utils.cpp
+++ b/src/posix/platform/utils.cpp
@@ -55,7 +55,7 @@
     char    cmd[kSystemCommandMaxLength];
     char    buf[kOutputBufferSize];
     va_list args;
-    FILE *  file;
+    FILE   *file;
     int     exitCode;
     otError error = OT_ERROR_NONE;
 
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 7ece234..aecd939 100644
--- a/src/posix/platform/vendor_interface.hpp
+++ b/src/posix/platform/vendor_interface.hpp
@@ -41,8 +41,6 @@
 #include "platform-posix.h"
 #include "lib/spinel/spinel_interface.hpp"
 
-#if OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_VENDOR
-
 namespace ot {
 namespace Posix {
 
@@ -50,7 +48,7 @@
  * This class defines a vendor interface to the Radio Co-processor (RCP).
  *
  */
-class VendorInterface
+class VendorInterface : public ot::Spinel::SpinelInterface
 {
 public:
     /**
@@ -62,8 +60,8 @@
      *
      */
     VendorInterface(Spinel::SpinelInterface::ReceiveFrameCallback aCallback,
-                    void *                                        aCallbackContext,
-                    Spinel::SpinelInterface::RxFrameBuffer &      aFrameBuffer);
+                    void                                         *aCallbackContext,
+                    Spinel::SpinelInterface::RxFrameBuffer       &aFrameBuffer);
 
     /**
      * This destructor deinitializes the object.
@@ -119,21 +117,18 @@
     /**
      * This method updates the file descriptor sets with file descriptors used by the radio driver.
      *
-     * @param[inout] aReadFdSet   A reference to the read file descriptors.
-     * @param[inout] aWriteFdSet  A reference to the write file descriptors.
-     * @param[inout] aMaxFd       A reference to the max file descriptor.
-     * @param[inout] aTimeout     A reference to the timeout.
+     * @param[in,out]   aMainloopContext  A pointer to the mainloop context containing fd_sets.
      *
      */
-    void UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, int &aMaxFd, struct timeval &aTimeout);
+    void UpdateFdSet(void *aMainloopContext);
 
     /**
      * This method performs radio driver processing.
      *
-     * @param[in] aContext  The context containing fd_sets.
+     * @param[in]   aMainloopContext  A pointer to the mainloop context containing fd_sets.
      *
      */
-    void Process(const RadioProcessContext &aContext);
+    void Process(const void *aMainloopContext);
 
     /**
      * This method returns the bus speed between the host and the radio.
@@ -144,19 +139,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.
@@ -164,11 +153,10 @@
      * @returns The RCP interface metrics.
      *
      */
-    const otRcpInterfaceMetrics *GetRcpInterfaceMetrics(void);
+    const otRcpInterfaceMetrics *GetRcpInterfaceMetrics(void) const;
 };
 
 } // namespace Posix
 } // namespace ot
 
-#endif // OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_VENDOR
 #endif // POSIX_APP_VENDOR_INTERFACE_HPP_
diff --git a/src/posix/platform/vendor_interface_example.cpp b/src/posix/platform/vendor_interface_example.cpp
index 12121d3..f45a932 100644
--- a/src/posix/platform/vendor_interface_example.cpp
+++ b/src/posix/platform/vendor_interface_example.cpp
@@ -50,8 +50,8 @@
 {
 public:
     explicit VendorInterfaceImpl(SpinelInterface::ReceiveFrameCallback aCallback,
-                                 void *                                aCallbackContext,
-                                 SpinelInterface::RxFrameBuffer &      aFrameBuffer)
+                                 void                                 *aCallbackContext,
+                                 SpinelInterface::RxFrameBuffer       &aFrameBuffer)
         : mReceiveFrameCallback(aCallback)
         , mReceiveFrameContext(aCallbackContext)
         , mRxFrameBuffer(aFrameBuffer)
@@ -65,8 +65,8 @@
 
 private:
     SpinelInterface::ReceiveFrameCallback mReceiveFrameCallback;
-    void *                                mReceiveFrameContext;
-    SpinelInterface::RxFrameBuffer &      mRxFrameBuffer;
+    void                                 *mReceiveFrameContext;
+    SpinelInterface::RxFrameBuffer       &mRxFrameBuffer;
 };
 
 // ----------------------------------------------------------------------------
@@ -76,17 +76,14 @@
 static OT_DEFINE_ALIGNED_VAR(sVendorInterfaceImplRaw, sizeof(VendorInterfaceImpl), uint64_t);
 
 VendorInterface::VendorInterface(SpinelInterface::ReceiveFrameCallback aCallback,
-                                 void *                                aCallbackContext,
-                                 SpinelInterface::RxFrameBuffer &      aFrameBuffer)
+                                 void                                 *aCallbackContext,
+                                 SpinelInterface::RxFrameBuffer       &aFrameBuffer)
 {
     new (&sVendorInterfaceImplRaw) VendorInterfaceImpl(aCallback, aCallbackContext, aFrameBuffer);
     OT_UNUSED_VARIABLE(sVendorInterfaceImplRaw);
 }
 
-VendorInterface::~VendorInterface(void)
-{
-    Deinit();
-}
+VendorInterface::~VendorInterface(void) { Deinit(); }
 
 otError VendorInterface::Init(const Url::Url &aRadioUrl)
 {
@@ -102,29 +99,25 @@
     // TODO: Implement vendor code here.
 }
 
-uint32_t VendorInterface::GetBusSpeed(void) const
-{
-    return 1000000;
-}
+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)
+void VendorInterface::UpdateFdSet(void *aMainloopContext)
 {
-    OT_UNUSED_VARIABLE(aReadFdSet);
-    OT_UNUSED_VARIABLE(aWriteFdSet);
-    OT_UNUSED_VARIABLE(aMaxFd);
-    OT_UNUSED_VARIABLE(aTimeout);
+    OT_UNUSED_VARIABLE(aMainloopContext);
 
     // TODO: Implement vendor code here.
 }
 
-void VendorInterface::Process(const RadioProcessContext &aContext)
+void VendorInterface::Process(const void *aMainloopContext)
 {
-    OT_UNUSED_VARIABLE(aContext);
+    OT_UNUSED_VARIABLE(aMainloopContext);
 
     // TODO: Implement vendor code here.
 }
@@ -148,14 +141,7 @@
     return OT_ERROR_NONE;
 }
 
-otError VendorInterface::ResetConnection(void)
-{
-    // TODO: Implement vendor code here.
-
-    return OT_ERROR_NONE;
-}
-
-const otRcpInterfaceMetrics *VendorInterface::GetRcpInterfaceMetrics(void)
+const otRcpInterfaceMetrics *VendorInterface::GetRcpInterfaceMetrics(void) const
 {
     // TODO: Implement vendor code here.
 
diff --git a/src/posix/platform/virtual_time.cpp b/src/posix/platform/virtual_time.cpp
index b6faf38..e278ed4 100644
--- a/src/posix/platform/virtual_time.cpp
+++ b/src/posix/platform/virtual_time.cpp
@@ -55,7 +55,7 @@
 void virtualTimeInit(uint16_t aNodeId)
 {
     struct sockaddr_in sockaddr;
-    char *             offset;
+    char              *offset;
 
     memset(&sockaddr, 0, sizeof(sockaddr));
     sockaddr.sin_family = AF_INET;
@@ -159,37 +159,24 @@
     virtualTimeSendEvent(&event, offsetof(struct VirtualTimeEvent, mData) + event.mDataLength);
 }
 
-void virtualTimeUpdateFdSet(fd_set *        aReadFdSet,
-                            fd_set *        aWriteFdSet,
-                            fd_set *        aErrorFdSet,
-                            int *           aMaxFd,
-                            struct timeval *aTimeout)
+void virtualTimeUpdateFdSet(otSysMainloopContext *aContext)
 {
-    OT_UNUSED_VARIABLE(aWriteFdSet);
-    OT_UNUSED_VARIABLE(aErrorFdSet);
-    OT_UNUSED_VARIABLE(aTimeout);
-
-    FD_SET(sSockFd, aReadFdSet);
-    if (*aMaxFd < sSockFd)
+    FD_SET(sSockFd, &aContext->mReadFdSet);
+    if (aContext->mMaxFd < sSockFd)
     {
-        *aMaxFd = sSockFd;
+        aContext->mMaxFd = sSockFd;
     }
 }
 
-void virtualTimeProcess(otInstance *  aInstance,
-                        const fd_set *aReadFdSet,
-                        const fd_set *aWriteFdSet,
-                        const fd_set *aErrorFdSet)
+void virtualTimeProcess(otInstance *aInstance, const otSysMainloopContext *aContext)
 {
     struct VirtualTimeEvent event;
 
     memset(&event, 0, sizeof(event));
 
     OT_UNUSED_VARIABLE(aInstance);
-    OT_UNUSED_VARIABLE(aWriteFdSet);
-    OT_UNUSED_VARIABLE(aErrorFdSet);
 
-    if (FD_ISSET(sSockFd, aReadFdSet))
+    if (FD_ISSET(sSockFd, &aContext->mReadFdSet))
     {
         virtualTimeReceiveEvent(&event);
     }
@@ -197,9 +184,6 @@
     virtualTimeRadioSpinelProcess(aInstance, &event);
 }
 
-uint64_t otPlatTimeGet(void)
-{
-    return sNow;
-}
+uint64_t otPlatTimeGet(void) { return sNow; }
 
 #endif // OPENTHREAD_POSIX_VIRTUAL_TIME
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 2b00f1c..8405376 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -26,10 +26,8 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-if(OT_PLATFORM STREQUAL "simulation")
-    if(OT_FTD)
-        add_subdirectory(unit)
-    endif()
+if(OT_FTD AND BUILD_TESTING)
+    add_subdirectory(unit)
 endif()
 
 option(OT_FUZZ_TARGETS "enable fuzz targets" OFF)
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 7d0459c..462f908 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -31,8 +31,6 @@
 # Always package (e.g. for 'make dist') these subdirectories.
 
 DIST_SUBDIRS                            = \
-    unit                                  \
-    scripts                               \
     fuzz                                  \
     $(NULL)
 
@@ -41,49 +39,8 @@
 SUBDIRS                                 = \
     $(NULL)
 
-SUBDIRS                                += \
-    unit                                  \
-    $(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/cli_received.cpp b/tests/fuzz/cli_received.cpp
index 2dcbbd8..a8a2a10 100644
--- a/tests/fuzz/cli_received.cpp
+++ b/tests/fuzz/cli_received.cpp
@@ -26,8 +26,6 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#define MAX_ITERATIONS 100
-
 #include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -40,9 +38,11 @@
 #include <openthread/tasklet.h>
 #include <openthread/thread.h>
 #include <openthread/thread_ftd.h>
+#include <openthread/platform/alarm-milli.h>
 
 #include "fuzzer_platform.h"
 #include "common/code_utils.hpp"
+#include "common/time.hpp"
 
 static int CliOutput(void *aContext, const char *aFormat, va_list aArguments)
 {
@@ -53,12 +53,27 @@
     return vsnprintf(nullptr, 0, aFormat, aArguments);
 }
 
+void AdvanceTime(otInstance *aInstance, uint32_t aDuration)
+{
+    uint32_t time = otPlatAlarmMilliGetNow() + aDuration;
+
+    while (ot::TimeMilli(otPlatAlarmMilliGetNow()) <= ot::TimeMilli(time))
+    {
+        while (otTaskletsArePending(aInstance))
+        {
+            otTaskletsProcess(aInstance);
+        }
+
+        FuzzerPlatformProcess(aInstance);
+    }
+}
+
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
 {
     const otPanId panId = 0xdead;
 
     otInstance *instance = nullptr;
-    uint8_t *   buf      = nullptr;
+    uint8_t    *buf      = nullptr;
 
     VerifyOrExit(size <= 65536);
 
@@ -71,8 +86,9 @@
     IgnoreError(otThreadSetEnabled(instance, true));
     IgnoreError(otThreadBecomeLeader(instance));
 
-    buf = static_cast<uint8_t *>(malloc(size + 1));
+    AdvanceTime(instance, 10000);
 
+    buf = static_cast<uint8_t *>(malloc(size + 1));
     memcpy(buf, data, size);
     buf[size] = '\0';
 
@@ -80,15 +96,7 @@
 
     VerifyOrExit(!FuzzerPlatformResetWasRequested());
 
-    for (int i = 0; i < MAX_ITERATIONS; i++)
-    {
-        while (otTaskletsArePending(instance))
-        {
-            otTaskletsProcess(instance);
-        }
-
-        FuzzerPlatformProcess(instance);
-    }
+    AdvanceTime(instance, 10000);
 
 exit:
 
diff --git a/tests/fuzz/fuzzer_platform.cpp b/tests/fuzz/fuzzer_platform.cpp
index 7449937..012cee4 100644
--- a/tests/fuzz/fuzzer_platform.cpp
+++ b/tests/fuzz/fuzzer_platform.cpp
@@ -144,15 +144,9 @@
     }
 }
 
-bool FuzzerPlatformResetWasRequested(void)
-{
-    return sResetWasRequested;
-}
+bool FuzzerPlatformResetWasRequested(void) { return sResetWasRequested; }
 
-uint32_t otPlatAlarmMilliGetNow(void)
-{
-    return sAlarmNow / 1000;
-}
+uint32_t otPlatAlarmMilliGetNow(void) { return sAlarmNow / 1000; }
 
 void otPlatAlarmMilliStartAt(otInstance *aInstance, uint32_t aT0, uint32_t aDt)
 {
@@ -169,10 +163,7 @@
     sAlarmMilli.isRunning = false;
 }
 
-uint32_t otPlatAlarmMicroGetNow(void)
-{
-    return sAlarmNow;
-}
+uint32_t otPlatAlarmMicroGetNow(void) { return sAlarmNow; }
 
 void otPlatAlarmMicroStartAt(otInstance *aInstance, uint32_t aT0, uint32_t aDt)
 {
@@ -207,12 +198,14 @@
     return OT_ERROR_NOT_IMPLEMENTED;
 }
 
-void otDiagProcessCmdLine(otInstance *aInstance, const char *aString, char *aOutput, size_t aOutputMaxLen)
+otError otDiagProcessCmdLine(otInstance *aInstance, const char *aString, char *aOutput, size_t aOutputMaxLen)
 {
     OT_UNUSED_VARIABLE(aInstance);
     OT_UNUSED_VARIABLE(aString);
     OT_UNUSED_VARIABLE(aOutput);
     OT_UNUSED_VARIABLE(aOutputMaxLen);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
 }
 
 void otPlatReset(otInstance *aInstance)
@@ -228,16 +221,14 @@
     return OT_PLAT_RESET_REASON_POWER_ON;
 }
 
-void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, ...)
+OT_TOOL_WEAK void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, ...)
 {
     OT_UNUSED_VARIABLE(aLogLevel);
     OT_UNUSED_VARIABLE(aLogRegion);
     OT_UNUSED_VARIABLE(aFormat);
 }
 
-void otPlatWakeHost(void)
-{
-}
+void otPlatWakeHost(void) {}
 
 void otPlatRadioGetIeeeEui64(otInstance *aInstance, uint8_t *aIeeeEui64)
 {
@@ -386,15 +377,9 @@
     return OT_ERROR_NONE;
 }
 
-void otPlatRadioClearSrcMatchShortEntries(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
+void otPlatRadioClearSrcMatchShortEntries(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
 
-void otPlatRadioClearSrcMatchExtEntries(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
+void otPlatRadioClearSrcMatchExtEntries(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
 
 otError otPlatRadioEnergyScan(otInstance *aInstance, uint8_t aScanChannel, uint16_t aScanDuration)
 {
@@ -448,10 +433,7 @@
     OT_UNUSED_VARIABLE(aSensitiveKeysLength);
 }
 
-void otPlatSettingsDeinit(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
+void otPlatSettingsDeinit(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
 
 otError otPlatSettingsGet(otInstance *aInstance, uint16_t aKey, int aIndex, uint8_t *aValue, uint16_t *aValueLength)
 {
@@ -489,15 +471,12 @@
     return OT_ERROR_NONE;
 }
 
-void otPlatSettingsWipe(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
+void otPlatSettingsWipe(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
 
 otError otPlatDiagProcess(otInstance *aInstance,
                           uint8_t     aArgsLength,
-                          char *      aArgs[],
-                          char *      aOutput,
+                          char       *aArgs[],
+                          char       *aOutput,
                           size_t      aOutputMaxLen)
 {
     OT_UNUSED_VARIABLE(aInstance);
@@ -509,25 +488,13 @@
     return OT_ERROR_INVALID_COMMAND;
 }
 
-void otPlatDiagModeSet(bool aMode)
-{
-    OT_UNUSED_VARIABLE(aMode);
-}
+void otPlatDiagModeSet(bool aMode) { OT_UNUSED_VARIABLE(aMode); }
 
-bool otPlatDiagModeGet(void)
-{
-    return false;
-}
+bool otPlatDiagModeGet(void) { return false; }
 
-void otPlatDiagChannelSet(uint8_t aChannel)
-{
-    OT_UNUSED_VARIABLE(aChannel);
-}
+void otPlatDiagChannelSet(uint8_t aChannel) { OT_UNUSED_VARIABLE(aChannel); }
 
-void otPlatDiagTxPowerSet(int8_t aTxPower)
-{
-    OT_UNUSED_VARIABLE(aTxPower);
-}
+void otPlatDiagTxPowerSet(int8_t aTxPower) { OT_UNUSED_VARIABLE(aTxPower); }
 
 void otPlatDiagRadioReceived(otInstance *aInstance, otRadioFrame *aFrame, otError aError)
 {
@@ -536,7 +503,4 @@
     OT_UNUSED_VARIABLE(aError);
 }
 
-void otPlatDiagAlarmCallback(otInstance *aInstance)
-{
-    OT_UNUSED_VARIABLE(aInstance);
-}
+void otPlatDiagAlarmCallback(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); }
diff --git a/tests/fuzz/ip6_send.cpp b/tests/fuzz/ip6_send.cpp
index da2cfb3..ef366a0 100644
--- a/tests/fuzz/ip6_send.cpp
+++ b/tests/fuzz/ip6_send.cpp
@@ -26,27 +26,43 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#define MAX_ITERATIONS 100
-
 #include <stddef.h>
 
 #include <openthread/instance.h>
 #include <openthread/ip6.h>
 #include <openthread/link.h>
 #include <openthread/message.h>
+#include <openthread/srp_server.h>
 #include <openthread/tasklet.h>
 #include <openthread/thread.h>
 #include <openthread/thread_ftd.h>
+#include <openthread/platform/alarm-milli.h>
 
 #include "fuzzer_platform.h"
 #include "common/code_utils.hpp"
+#include "common/time.hpp"
+
+void AdvanceTime(otInstance *aInstance, uint32_t aDuration)
+{
+    uint32_t time = otPlatAlarmMilliGetNow() + aDuration;
+
+    while (ot::TimeMilli(otPlatAlarmMilliGetNow()) <= ot::TimeMilli(time))
+    {
+        while (otTaskletsArePending(aInstance))
+        {
+            otTaskletsProcess(aInstance);
+        }
+
+        FuzzerPlatformProcess(aInstance);
+    }
+}
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
 {
     const otPanId panId = 0xdead;
 
-    otInstance *      instance = nullptr;
-    otMessage *       message  = nullptr;
+    otInstance       *instance = nullptr;
+    otMessage        *message  = nullptr;
     otError           error    = OT_ERROR_NONE;
     otMessageSettings settings;
 
@@ -58,8 +74,11 @@
     IgnoreError(otLinkSetPanId(instance, panId));
     IgnoreError(otIp6SetEnabled(instance, true));
     IgnoreError(otThreadSetEnabled(instance, true));
+    otSrpServerSetEnabled(instance, true);
     IgnoreError(otThreadBecomeLeader(instance));
 
+    AdvanceTime(instance, 10000);
+
     settings.mLinkSecurityEnabled = (data[0] & 0x1) != 0;
     settings.mPriority            = OT_MESSAGE_PRIORITY_NORMAL;
 
@@ -75,15 +94,7 @@
 
     VerifyOrExit(!FuzzerPlatformResetWasRequested());
 
-    for (int i = 0; i < MAX_ITERATIONS; i++)
-    {
-        while (otTaskletsArePending(instance))
-        {
-            otTaskletsProcess(instance);
-        }
-
-        FuzzerPlatformProcess(instance);
-    }
+    AdvanceTime(instance, 10000);
 
 exit:
 
diff --git a/tests/fuzz/ncp_hdlc_received.cpp b/tests/fuzz/ncp_hdlc_received.cpp
index a61b0fa..8afb417 100644
--- a/tests/fuzz/ncp_hdlc_received.cpp
+++ b/tests/fuzz/ncp_hdlc_received.cpp
@@ -26,8 +26,6 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#define MAX_ITERATIONS 100
-
 #include <stdlib.h>
 #include <string.h>
 
@@ -35,12 +33,15 @@
 #include <openthread/ip6.h>
 #include <openthread/link.h>
 #include <openthread/ncp.h>
+#include <openthread/srp_server.h>
 #include <openthread/tasklet.h>
 #include <openthread/thread.h>
 #include <openthread/thread_ftd.h>
+#include <openthread/platform/alarm-milli.h>
 
 #include "fuzzer_platform.h"
 #include "common/code_utils.hpp"
+#include "common/time.hpp"
 
 static int HdlcSend(const uint8_t *aBuf, uint16_t aBufLength)
 {
@@ -50,12 +51,27 @@
     return aBufLength;
 }
 
+void AdvanceTime(otInstance *aInstance, uint32_t aDuration)
+{
+    uint32_t time = otPlatAlarmMilliGetNow() + aDuration;
+
+    while (ot::TimeMilli(otPlatAlarmMilliGetNow()) <= ot::TimeMilli(time))
+    {
+        while (otTaskletsArePending(aInstance))
+        {
+            otTaskletsProcess(aInstance);
+        }
+
+        FuzzerPlatformProcess(aInstance);
+    }
+}
+
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
 {
     const otPanId panId = 0xdead;
 
     otInstance *instance = nullptr;
-    uint8_t *   buf      = nullptr;
+    uint8_t    *buf      = nullptr;
 
     VerifyOrExit(size <= 65536);
 
@@ -66,25 +82,18 @@
     IgnoreError(otLinkSetPanId(instance, panId));
     IgnoreError(otIp6SetEnabled(instance, true));
     IgnoreError(otThreadSetEnabled(instance, true));
+    otSrpServerSetEnabled(instance, true);
     IgnoreError(otThreadBecomeLeader(instance));
 
+    AdvanceTime(instance, 10000);
+
     buf = static_cast<uint8_t *>(malloc(size));
-
     memcpy(buf, data, size);
-
     otNcpHdlcReceive(buf, static_cast<uint16_t>(size));
 
     VerifyOrExit(!FuzzerPlatformResetWasRequested());
 
-    for (int i = 0; i < MAX_ITERATIONS; i++)
-    {
-        while (otTaskletsArePending(instance))
-        {
-            otTaskletsProcess(instance);
-        }
-
-        FuzzerPlatformProcess(instance);
-    }
+    AdvanceTime(instance, 10000);
 
 exit:
 
diff --git a/tests/fuzz/oss-fuzz-build b/tests/fuzz/oss-fuzz-build
index 31373c4..c8bb407 100755
--- a/tests/fuzz/oss-fuzz-build
+++ b/tests/fuzz/oss-fuzz-build
@@ -27,6 +27,8 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
+set -euxo pipefail
+
 (
     mkdir build
     cd build || exit
@@ -43,7 +45,6 @@
         -DOT_BORDER_ROUTER=ON \
         -DOT_CHANNEL_MANAGER=ON \
         -DOT_CHANNEL_MONITOR=ON \
-        -DOT_CHILD_SUPERVISION=ON \
         -DOT_COAP=ON \
         -DOT_COAPS=ON \
         -DOT_COAP_BLOCK=ON \
@@ -61,15 +62,15 @@
         -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 \
         -DOT_SNTP_CLIENT=ON \
         -DOT_SRP_CLIENT=ON \
         -DOT_SRP_SERVER=ON \
-        -DOT_THREAD_VERSION=1.2 \
+        -DOT_THREAD_VERSION=1.3 \
         -DOT_UPTIME=ON \
         ..
     ninja
diff --git a/tests/fuzz/ot_fuzz_framework.cpp b/tests/fuzz/ot_fuzz_framework.cpp
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/fuzz/ot_fuzz_framework.cpp
diff --git a/tests/fuzz/ot_fuzz_framework.h b/tests/fuzz/ot_fuzz_framework.h
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/fuzz/ot_fuzz_framework.h
diff --git a/tests/fuzz/radio_receive_done.cpp b/tests/fuzz/radio_receive_done.cpp
index d30b4ae..04399cd 100644
--- a/tests/fuzz/radio_receive_done.cpp
+++ b/tests/fuzz/radio_receive_done.cpp
@@ -26,8 +26,6 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#define MAX_ITERATIONS 100
-
 #include <stdlib.h>
 #include <string.h>
 
@@ -37,18 +35,35 @@
 #include <openthread/tasklet.h>
 #include <openthread/thread.h>
 #include <openthread/thread_ftd.h>
+#include <openthread/platform/alarm-milli.h>
 #include <openthread/platform/radio.h>
 
 #include "fuzzer_platform.h"
 #include "common/code_utils.hpp"
+#include "common/time.hpp"
+
+void AdvanceTime(otInstance *aInstance, uint32_t aDuration)
+{
+    uint32_t time = otPlatAlarmMilliGetNow() + aDuration;
+
+    while (ot::TimeMilli(otPlatAlarmMilliGetNow()) <= ot::TimeMilli(time))
+    {
+        while (otTaskletsArePending(aInstance))
+        {
+            otTaskletsProcess(aInstance);
+        }
+
+        FuzzerPlatformProcess(aInstance);
+    }
+}
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
 {
     const otPanId panId = 0xdead;
 
-    otInstance * instance = nullptr;
+    otInstance  *instance = nullptr;
     otRadioFrame frame;
-    uint8_t *    buf = nullptr;
+    uint8_t     *buf = nullptr;
 
     VerifyOrExit(size <= OT_RADIO_FRAME_MAX_SIZE);
 
@@ -60,28 +75,20 @@
     IgnoreError(otThreadSetEnabled(instance, true));
     IgnoreError(otThreadBecomeLeader(instance));
 
-    buf = static_cast<uint8_t *>(malloc(size));
+    AdvanceTime(instance, 10000);
 
+    buf = static_cast<uint8_t *>(malloc(size));
     memset(&frame, 0, sizeof(frame));
     frame.mPsdu    = buf;
     frame.mChannel = 11;
     frame.mLength  = static_cast<uint8_t>(size);
-
     memcpy(buf, data, frame.mLength);
 
     otPlatRadioReceiveDone(instance, &frame, OT_ERROR_NONE);
 
     VerifyOrExit(!FuzzerPlatformResetWasRequested());
 
-    for (int i = 0; i < MAX_ITERATIONS; i++)
-    {
-        while (otTaskletsArePending(instance))
-        {
-            otTaskletsProcess(instance);
-        }
-
-        FuzzerPlatformProcess(instance);
-    }
+    AdvanceTime(instance, 10000);
 
 exit:
 
diff --git a/tests/scripts/Makefile.am b/tests/scripts/Makefile.am
deleted file mode 100644
index d1cd8f1..0000000
--- a/tests/scripts/Makefile.am
+++ /dev/null
@@ -1,51 +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
-
-# Always package (e.g. for 'make dist') these subdirectories.
-
-DIST_SUBDIRS                            = \
-    thread-cert                           \
-    $(NULL)
-
-# Always build (e.g. for 'make all') these subdirectories.
-
-SUBDIRS                                 = \
-    $(NULL)
-
-if OPENTHREAD_POSIX
-if OPENTHREAD_ENABLE_CLI
-SUBDIRS                                += \
-    thread-cert                           \
-    $(NULL)
-endif
-endif
-
-include $(abs_top_nlbuild_autotools_dir)/automake/post.am
-
diff --git a/tests/scripts/expect/_common.exp b/tests/scripts/expect/_common.exp
index 3cdb89b..820a0c6 100644
--- a/tests/scripts/expect/_common.exp
+++ b/tests/scripts/expect/_common.exp
@@ -37,7 +37,7 @@
 
 proc wait_for {command success {failure {[\r\n]FAILURE_NOT_EXPECTED[\r\n]}}} {
     set timeout 1
-    for {set i 0} {$i < 20} {incr i} {
+    for {set i 0} {$i < 40} {incr i} {
         if {$command != ""} {
             send "$command\n"
         }
@@ -126,6 +126,15 @@
     set spawn_id $spawn_ids($id)
 }
 
+proc attach {{role "leader"}} {
+    send "ifconfig up\n"
+    expect_line "Done"
+    send "thread start\n"
+    expect_line "Done"
+    wait_for "state" $role
+    expect_line "Done"
+}
+
 proc setup_leader {} {
     send "dataset init new\n"
     expect_line "Done"
@@ -133,12 +142,7 @@
     expect_line "Done"
     send "dataset commit active\n"
     expect_line "Done"
-    send "ifconfig up\n"
-    expect_line "Done"
-    send "thread start\n"
-    expect_line "Done"
-    wait_for "state" "leader"
-    expect_line "Done"
+    attach
 }
 
 proc dispose_node {id} {
@@ -173,6 +177,23 @@
     return $rval
 }
 
+proc get_extpanid {} {
+    send "extpanid\n"
+    set rval [expect_line {[0-9a-fA-F]{16}}]
+    expect_line "Done"
+
+    return $rval
+}
+
+proc get_panid {} {
+    send "panid\n"
+    expect -re {0x([0-9a-fA-F]{4})}
+    set rval $expect_out(1,string)
+    expect_line "Done"
+
+    return $rval
+}
+
 proc get_meshlocal_prefix {} {
     send "prefix meshlocal\n"
     expect -re {[\r\n ](([0-9a-fA-F]{1,4}:){3}[0-9a-fA-f]{1,4})::/64(?=[\r\n>])}
diff --git a/tests/scripts/expect/cli-child.exp b/tests/scripts/expect/cli-child.exp
index 1e73099..af13b2e 100755
--- a/tests/scripts/expect/cli-child.exp
+++ b/tests/scripts/expect/cli-child.exp
@@ -38,9 +38,9 @@
 
 switch_node 1
 send "child table\n"
-expect "| ID  | RLOC16 | Timeout    | Age        | LQ In | C_VN |R|D|N|Ver|CSL|QMsgCnt| Extended MAC     |"
-expect "+-----+--------+------------+------------+-------+------+-+-+-+---+---+-------+------------------+"
-expect -re "\\| +(\\d+) \\| 0x$rloc \\| +\\d+ \\| +\\d+ \\| +\\d+ \\| +\\d+ \\|\\d\\|\\d\\|\\d\\| *\\d+\\| \\d \\| +\\d+ \\| $extaddr \\|"
+expect "| ID  | RLOC16 | Timeout    | Age        | LQ In | C_VN |R|D|N|Ver|CSL|QMsgCnt|Suprvsn| Extended MAC     |"
+expect "+-----+--------+------------+------------+-------+------+-+-+-+---+---+-------+-------+------------------+"
+expect -re "\\| +(\\d+) \\| 0x$rloc \\| +\\d+ \\| +\\d+ \\| +\\d+ \\| +\\d+ \\|\\d\\|\\d\\|\\d\\| *\\d+\\| \\d \\| +\\d+ \\| +\\d+ \\| $extaddr \\|"
 set child_id $expect_out(1,string)
 expect_line "Done"
 send "child list\n"
diff --git a/tests/scripts/expect/cli-diags.exp b/tests/scripts/expect/cli-diags.exp
new file mode 100755
index 0000000..e5d2fbf
--- /dev/null
+++ b/tests/scripts/expect/cli-diags.exp
@@ -0,0 +1,214 @@
+#!/usr/bin/expect -f
+#
+#  Copyright (c) 2022, 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.
+#
+
+source "tests/scripts/expect/_common.exp"
+source "tests/scripts/expect/_multinode.exp"
+
+spawn_node 1
+spawn_node 2
+
+switch_node 1
+send "diag start\n"
+expect "start diagnostics mode"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag channel 11\n"
+expect "set channel to 11"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag stats clear\n"
+expect "stats cleared"
+expect_line "Done"
+
+switch_node 2
+
+send "diag start\n"
+expect "start diagnostics mode"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag channel 11\n"
+expect "set channel to 11"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag stats clear\n"
+expect "stats cleared"
+expect_line "Done"
+
+send "diag send 10 100\n"
+expect "sending 0xa packet(s), length 0x64"
+expect "status 0x00"
+expect_line "Done"
+
+sleep 2
+
+send "diag stats\n"
+expect "received packets: 0"
+expect "sent packets: 10"
+expect "first received packet: rssi=0, lqi=0"
+expect "last received packet: rssi=0, lqi=0"
+expect_line "Done"
+
+switch_node 1
+
+send "diag stats\n"
+expect "received packets: 10"
+expect "sent packets: 0"
+expect "first received packet: rssi=-20, lqi=0"
+expect "last received packet: rssi=-20, lqi=0"
+expect_line "Done"
+
+send "diag stats clear\n"
+expect "stats cleared"
+expect_line "Done"
+
+switch_node 2
+
+send "diag repeat 20 100\n"
+expect "sending packets of length 0x64 at the delay of 0x14 ms"
+expect "status 0x00"
+expect_line "Done"
+sleep 1
+send "diag repeat stop\n"
+expect "repeated packet transmission is stopped"
+expect "status 0x00"
+expect_line "Done"
+
+switch_node 1
+
+send "diag stats\n"
+expect -r {received packets: \d+}
+expect "sent packets: 0"
+expect "first received packet: rssi=-20, lqi=0"
+expect "last received packet: rssi=-20, lqi=0"
+expect_line "Done"
+
+send "diag stats clear\n"
+expect "stats cleared"
+expect_line "Done"
+
+dispose_all
+
+
+spawn_node 1
+
+send "diag start\n"
+expect "start diagnostics mode"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag channel 11\n"
+expect "set channel to 11"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag power 10\n"
+expect "set tx power to 10 dBm"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag radio sleep\n"
+expect "set radio from receive to sleep"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag radio state\n"
+expect "sleep"
+expect_line "Done"
+
+send "diag radio receive\n"
+expect "set radio from sleep to receive on channel 11"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag radio state\n"
+expect "receive"
+expect_line "Done"
+
+send "diag gpio set 0 1\n"
+expect_line "Done"
+
+send "diag gpio get 0\n"
+expect "1"
+expect_line "Done"
+
+send "diag gpio mode 0 in\n"
+expect_line "Done"
+
+send "diag gpio mode 0\n"
+expect "in"
+expect_line "Done"
+
+send "diag gpio mode 0 out\n"
+expect_line "Done"
+
+send "diag gpio mode 0\n"
+expect "out"
+expect_line "Done"
+
+send "diag cw start\n"
+expect_line "Done"
+
+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"
+
+send "diag rawpowersetting\n"
+expect "112233"
+expect_line "Done"
+
+send "diag rawpowersetting enable\n"
+expect_line "Done"
+
+send "diag rawpowersetting disable\n"
+expect_line "Done"
+
+send "diag invalid_commad\n"
+expect "Error 35: InvalidCommand"
+
+send "diag stop\n"
+expect_line "Done"
+
+send "diag channel\n"
+expect "failed"
+expect "status 0xd"
+expect "Error 13: InvalidState"
+
+dispose_all
diff --git a/tests/scripts/expect/cli-discover.exp b/tests/scripts/expect/cli-discover.exp
new file mode 100755
index 0000000..6c769f7
--- /dev/null
+++ b/tests/scripts/expect/cli-discover.exp
@@ -0,0 +1,146 @@
+#!/usr/bin/expect -f
+#
+#  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.
+#
+
+source "tests/scripts/expect/_common.exp"
+source "tests/scripts/expect/_multinode.exp"
+
+for {set i 1} {$i <= 5} {incr i} {
+    spawn_node $i
+}
+
+switch_node 1
+
+send "dataset init new\n"
+expect_line "Done"
+send "dataset networkkey 00112233445566778899aabbccddeeff\n"
+expect_line "Done"
+send "dataset channel 12\n"
+expect_line "Done"
+send "dataset networkname OpenThread-ch12\n"
+expect_line "Done"
+send "dataset commit active\n"
+expect_line "Done"
+send "dataset active -x\n"
+expect -re {([0-9a-f]+)[\r\n]+Done}
+set dataset $expect_out(1,string) 
+attach
+
+set extaddr_1 [get_extaddr]
+set extpan_1 [get_extpanid]
+set pan_1 [get_panid]
+
+switch_node 2
+
+send "dataset init new\n"
+expect_line "Done"
+send "dataset channel 23\n"
+expect_line "Done"
+send "dataset networkname OpenThread-ch23\n"
+expect_line "Done"
+send "dataset commit active\n"
+expect_line "Done" 
+attach
+
+set extaddr_2 [get_extaddr]
+set extpan_2 [get_extpanid]
+set pan_2 [get_panid]
+
+switch_node 3
+
+send "dataset set active $dataset\n"
+expect_line "Done"
+send "mode r\n"
+expect_line "Done"
+attach "child"
+
+switch_node 4
+
+send "dataset set active $dataset\n"
+expect_line "Done"
+send "mode -\n"
+expect_line "Done"
+attach "child"
+
+for {set i 3} {$i <= 4} {incr i} {
+    switch_node $i
+
+    send "discover\n"
+    expect "| Network Name     | Extended PAN     | PAN  | MAC Address      | Ch | dBm | LQI |"
+    expect "+------------------+------------------+------+------------------+----+-----+-----+"
+    wait_for "" "\\| OpenThread-ch12 +\\| $extpan_1 \\| $pan_1 \\| $extaddr_1 \\| 12 \\| +-?\\d+ \\| +\\d \\|"
+    wait_for "" "\\| OpenThread-ch23 +\\| $extpan_2 \\| $pan_2 \\| $extaddr_2 \\| 23 \\| +-?\\d+ \\| +\\d \\|"
+    wait_for "" "Done"
+}
+
+if {$::env(THREAD_VERSION) != "1.1" && $::env(OT_NODE_TYPE) == "cli"} {
+    send "csl channel 12\n"
+    expect_line "Done"
+    send "csl period 3125\n"
+    expect_line "Done"
+
+    sleep 1
+
+    send "discover\n"
+    expect "| Network Name     | Extended PAN     | PAN  | MAC Address      | Ch | dBm | LQI |"
+    expect "+------------------+------------------+------+------------------+----+-----+-----+"
+    wait_for "" "\\| OpenThread-ch12 +\\| $extpan_1 \\| $pan_1 \\| $extaddr_1 \\| 12 \\| +-?\\d+ \\| +\\d \\|"
+    wait_for "" "\\| OpenThread-ch23 +\\| $extpan_2 \\| $pan_2 \\| $extaddr_2 \\| 23 \\| +-?\\d+ \\| +\\d \\|"
+    wait_for "" "Done"
+}
+
+switch_node 1
+send "discover reqcallback enable\n"
+expect_line "Done"
+
+switch_node 5
+send "discover\n"
+expect "Error 13: InvalidState"
+send "ifconfig up\n"
+expect_line "Done"
+send "discover 12\n"
+
+expect "| Network Name     | Extended PAN     | PAN  | MAC Address      | Ch | dBm | LQI |"
+expect "+------------------+------------------+------+------------------+----+-----+-----+"
+wait_for "" "\\| OpenThread-ch12 +\\| $extpan_1 \\| $pan_1 \\| $extaddr_1 \\| 12 \\| +-?\\d+ \\| +\\d \\|"
+wait_for "" "Done"
+
+switch_node 1
+expect -re {version=\d,joiner=0}
+
+switch_node 5
+send "ifconfig up\n"
+expect_line "Done"
+send "joiner start 123456\n"
+set timeout 10
+expect "NotFound"
+
+switch_node 1
+expect -re {version=\d,joiner=1}
+
+dispose_all
diff --git a/tests/scripts/expect/cli-multicast-loop.exp b/tests/scripts/expect/cli-multicast-loop.exp
index ef6c8b6..14f07bd 100755
--- a/tests/scripts/expect/cli-multicast-loop.exp
+++ b/tests/scripts/expect/cli-multicast-loop.exp
@@ -44,14 +44,7 @@
 send "panid 0xface\n"
 expect_line "Done"
 
-send "ifconfig up\n"
-expect_line "Done"
-
-send "thread start\n"
-expect_line "Done"
-
-wait_for "state" "leader"
-expect_line "Done"
+attach
 
 set extaddr1 [get_extaddr]
 
@@ -63,14 +56,8 @@
 send "panid 0xface\n"
 expect_line "Done"
 
-send "ifconfig up\n"
-expect_line "Done"
+attach "router"
 
-send "thread start\n"
-expect_line "Done"
-
-wait_for "state" "router"
-expect_line "Done"
 sleep 3
 
 get_rloc16
@@ -87,14 +74,8 @@
 send "macfilter addr denylist\n"
 expect_line "Done"
 
-send "ifconfig up\n"
-expect_line "Done"
+attach "router"
 
-send "thread start\n"
-expect_line "Done"
-
-wait_for "state" "router"
-expect_line "Done"
 sleep 4
 
 set extaddr3 [get_extaddr]
@@ -116,14 +97,7 @@
 send "macfilter addr allowlist\n"
 expect_line "Done"
 
-send "ifconfig up\n"
-expect_line "Done"
-
-send "thread start\n"
-expect_line "Done"
-
-wait_for "state" "child"
-expect_line "Done"
+attach "child"
 
 get_rloc16
 
diff --git a/tests/scripts/expect/cli-neighbor.exp b/tests/scripts/expect/cli-neighbor.exp
index 9f5dd9e..1b5a9de 100755
--- a/tests/scripts/expect/cli-neighbor.exp
+++ b/tests/scripts/expect/cli-neighbor.exp
@@ -38,9 +38,9 @@
 
 switch_node 1
 send "neighbor table\n"
-expect "| Role | RLOC16 | Age | Avg RSSI | Last RSSI |R|D|N| Extended MAC     |"
-expect "+------+--------+-----+----------+-----------+-+-+-+------------------+"
-expect -re "\\| +C +\\| 0x$rloc \\| +\\d+ \\| +-?\\d+ \\| +-?\\d+ \\|1\\|0\\|0\\| $extaddr \\|"
+expect "| Role | RLOC16 | Age | Avg RSSI | Last RSSI |R|D|N| Extended MAC     | Version |"
+expect "+------+--------+-----+----------+-----------+-+-+-+------------------+---------+"
+expect -re "\\| +C +\\| 0x$rloc \\| +\\d+ \\| +-?\\d+ \\| +-?\\d+ \\|1\\|0\\|0\\| $extaddr \\| +\\d+ \\|"
 expect_line "Done"
 send "neighbor list\n"
 expect "0x$rloc"
diff --git a/tests/scripts/expect/cli-router.exp b/tests/scripts/expect/cli-router.exp
index b7f1a56..11456af 100755
--- a/tests/scripts/expect/cli-router.exp
+++ b/tests/scripts/expect/cli-router.exp
@@ -52,6 +52,7 @@
 switch_node 2
 send "mode rdn\n"
 expect_line "Done"
+sleep 5
 send "state router\n"
 expect_line "Done"
 wait_for "router list" $router_id
diff --git a/tests/scripts/expect/cli-scan-discover.exp b/tests/scripts/expect/cli-scan.exp
similarity index 73%
rename from tests/scripts/expect/cli-scan-discover.exp
rename to tests/scripts/expect/cli-scan.exp
index 78226c7..1bb20eb 100755
--- a/tests/scripts/expect/cli-scan-discover.exp
+++ b/tests/scripts/expect/cli-scan.exp
@@ -41,17 +41,7 @@
 expect -re {([0-9a-f]{4})}
 set pan $expect_out(1,string)
 expect_line "Done"
-send "extpanid\n"
-expect "extpanid"
-expect -re {([0-9a-f]{16})}
-set extpan $expect_out(1,string)
-expect_line "Done"
 expect "> "
-send "networkname\n"
-expect "networkname"
-expect -re {[\r\n]([^\r\n]+?)[\r\n]}
-set network $expect_out(1,string)
-expect_line "Done"
 send "channel\n"
 expect "channel"
 expect -re {(\d+)}
@@ -77,28 +67,4 @@
 expect -re "\\| +$channel \\| +-?\\d+ \\|"
 expect_line "Done"
 
-switch_node 3
-send "discover\n"
-expect "Error 13: InvalidState"
-send "ifconfig up\n"
-expect_line "Done"
-send "discover $channel\n"
-expect "| Network Name     | Extended PAN     | PAN  | MAC Address      | Ch | dBm | LQI |"
-expect "+------------------+------------------+------+------------------+----+-----+-----+"
-wait_for "" "\\| $network +\\| $extpan \\| $pan \\| $extaddr \\| +$channel \\| +-?\\d+ \\| +\\d \\|"
-wait_for "" "Done"
-send "discover something_invalid\n"
-expect "Error 7: InvalidArgs"
-
-switch_node 1
-expect -re {version=\d,joiner=0}
-
-switch_node 3
-send "joiner start 123456\n"
-set timeout 10
-expect "NotFound"
-
-switch_node 1
-expect -re {version=\d,joiner=1}
-
 dispose_all
diff --git a/tests/scripts/expect/cli-tcp-tls.exp b/tests/scripts/expect/cli-tcp-tls.exp
new file mode 100644
index 0000000..ccc4424
--- /dev/null
+++ b/tests/scripts/expect/cli-tcp-tls.exp
@@ -0,0 +1,168 @@
+#!/usr/bin/expect -f
+#
+#  Copyright (c) 2022, 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.
+#
+
+source "tests/scripts/expect/_common.exp"
+source "tests/scripts/expect/_multinode.exp"
+
+spawn_node 2 "cli"
+spawn_node 1 "cli"
+setup_leader
+setup_node 2 "rnd" "router"
+
+switch_node 1
+send "tcp init tls\n"
+expect_line "Done"
+
+switch_node 2
+send "tcp init tls\n"
+expect_line "Done"
+set addr_2 [get_ipaddr mleid]
+send "tcp listen :: 30000\n"
+expect_line "Done"
+send "tcp stoplistening\n"
+expect_line "Done"
+
+switch_node 1
+send "tcp connect $addr_2 30000\n"
+expect_line "Done"
+expect "TCP: Connection refused"
+
+switch_node 2
+send "tcp listen :: 30000\n"
+expect_line "Done"
+
+switch_node 1
+set addr_1 [get_ipaddr mleid]
+send "tcp bind $addr_1 25000\n"
+expect_line "Done"
+send "tcp connect $addr_2 30000\n"
+expect_line "Done"
+expect "TCP: Connection established"
+expect "TLS Handshake Complete"
+
+switch_node 2
+expect "Accepted connection from \\\[$addr_1\\\]:25000"
+expect "TCP: Connection established"
+expect "TLS Handshake Complete"
+
+switch_node 1
+send "tcp send hello\n"
+expect_line "Done"
+
+switch_node 2
+expect "TLS: Received 5 bytes: hello"
+expect "(TCP: Received 26 bytes)"
+send "tcp send world\n"
+expect_line "Done"
+
+switch_node 1
+expect "TLS: Received 5 bytes: world"
+expect "(TCP: Received 26 bytes)"
+send "tcp sendend\n"
+expect_line "Done"
+send "tcp send more\n"
+expect_line "Error 1: Failed"
+
+switch_node 2
+expect "TCP: Reached end of stream"
+send "tcp send goodbye\n"
+expect_line "Done"
+
+switch_node 1
+expect "TLS: Received 7 bytes: goodbye"
+expect "(TCP: Received 28 bytes)"
+
+switch_node 2
+send "tcp sendend\n"
+expect_line "Done"
+expect "TCP: Disconnected"
+
+switch_node 1
+expect "TCP: Reached end of stream"
+expect "TCP: Entered TIME-WAIT state"
+set timeout 130
+expect "TCP: Disconnected"
+
+# Test second connection with roles reversed (to be sure we're managing TLS state correctly)
+
+send "tcp listen :: 30000\n"
+expect_line "Done"
+
+switch_node 2
+send "tcp bind $addr_2 25000\n"
+expect_line "Done"
+send "tcp connect $addr_1 30000\n"
+expect_line "Done"
+expect "TCP: Connection established"
+expect "TLS Handshake Complete"
+
+switch_node 1
+expect "Accepted connection from \\\[$addr_2\\\]:25000"
+expect "TCP: Connection established"
+expect "TLS Handshake Complete"
+
+switch_node 2
+send "tcp send hello\n"
+expect_line "Done"
+
+switch_node 1
+expect "TLS: Received 5 bytes: hello"
+expect "(TCP: Received 26 bytes)"
+send "tcp send world\n"
+expect_line "Done"
+
+switch_node 2
+expect "TLS: Received 5 bytes: world"
+expect "(TCP: Received 26 bytes)"
+send "tcp sendend\n"
+expect_line "Done"
+send "tcp send more\n"
+expect_line "Error 1: Failed"
+
+switch_node 1
+expect "TCP: Reached end of stream"
+send "tcp send goodbye\n"
+expect_line "Done"
+
+switch_node 2
+expect "TLS: Received 7 bytes: goodbye"
+expect "(TCP: Received 28 bytes)"
+
+switch_node 1
+send "tcp sendend\n"
+expect_line "Done"
+expect "TCP: Disconnected"
+
+switch_node 2
+expect "TCP: Reached end of stream"
+expect "TCP: Entered TIME-WAIT state"
+set timeout 130
+expect "TCP: Disconnected"
+
+dispose_all
diff --git a/tests/scripts/expect/cli-tcp.exp b/tests/scripts/expect/cli-tcp.exp
index 838730d..6ae0385 100755
--- a/tests/scripts/expect/cli-tcp.exp
+++ b/tests/scripts/expect/cli-tcp.exp
@@ -30,18 +30,32 @@
 source "tests/scripts/expect/_common.exp"
 source "tests/scripts/expect/_multinode.exp"
 
-setup_two_nodes
+spawn_node 2 "cli"
+spawn_node 1 "cli"
+setup_leader
+setup_node 2 "rnd" "router"
 
 switch_node 1
-send "tcp init\n"
+send "tcp init circular\n"
 expect_line "Done"
 
 switch_node 2
-send "tcp init\n"
+send "tcp init linked\n"
 expect_line "Done"
 set addr_2 [get_ipaddr mleid]
 send "tcp listen :: 30000\n"
 expect_line "Done"
+send "tcp stoplistening\n"
+expect_line "Done"
+
+switch_node 1
+send "tcp connect $addr_2 30000\n"
+expect_line "Done"
+expect "TCP: Connection refused"
+
+switch_node 2
+send "tcp listen :: 30000\n"
+expect_line "Done"
 
 switch_node 1
 set addr_1 [get_ipaddr mleid]
diff --git a/tests/scripts/expect/cli-udp.exp b/tests/scripts/expect/cli-udp.exp
index 0711172..09e76d1 100755
--- a/tests/scripts/expect/cli-udp.exp
+++ b/tests/scripts/expect/cli-udp.exp
@@ -68,7 +68,7 @@
 send "udp send -x something_invalid\n"
 expect "Error 7: InvalidArgs"
 send "udp\n"
-expect "Error 7: InvalidArgs"
+expect "Error 35: InvalidCommand"
 
 send "udp linksecurity\n"
 expect "Enabled"
diff --git a/.lgtm.yml b/tests/scripts/expect/ot-fct.exp
old mode 100644
new mode 100755
similarity index 72%
copy from .lgtm.yml
copy to tests/scripts/expect/ot-fct.exp
index 9051e95..5fff5d9
--- a/.lgtm.yml
+++ b/tests/scripts/expect/ot-fct.exp
@@ -1,5 +1,6 @@
+#!/usr/bin/expect -f
 #
-#  Copyright (c) 2020, The OpenThread Authors.
+#  Copyright (c) 2022, The OpenThread Authors.
 #  All rights reserved.
 #
 #  Redistribution and use in source and binary forms, with or without
@@ -26,12 +27,22 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-extraction:
-  cpp:
-    prepare:
-      packages:
-        - cmake
-        - ninja-build
-    index:
-      build_command:
-        - THREAD_VERSION=1.2 NODE_MODE=rcp OT_NATIVE_IP=1 ./script/test build
+source "tests/scripts/expect/_common.exp"
+
+spawn build/posix/tools/ot-fct/ot-fct
+
+send "targetpowertable\n"
+expect_line "Done"
+send "powercalibrationtable\n"
+expect_line "Done"
+send "powercalibrationtable add -b 11,25 -c 1900,112233/1000,223344  -b 26,26 -c 1500,334455/700,445566\n"
+expect_line "Done"
+send "powercalibrationtable clear\n"
+expect_line "Done"
+send "regiondomaintable\n"
+expect "Done"
+send "invalidcommand\n"
+expect "failed"
+expect "status 0x17"
+send "\x04"
+expect eof
diff --git a/tests/scripts/expect/posix-diag-rcp.exp b/tests/scripts/expect/posix-diag-rcp.exp
index 08b9553..e585d60 100755
--- a/tests/scripts/expect/posix-diag-rcp.exp
+++ b/tests/scripts/expect/posix-diag-rcp.exp
@@ -42,21 +42,15 @@
 expect "diagnostics mode is enabled"
 expect_line "Done"
 send "diag rcp channel\n"
-expect "failed"
-expect "status 0x7"
-expect_line "Done"
+expect "Error 7: InvalidArgs"
 send "diag rcp channel 11\n"
 expect_line "Done"
 send "diag rcp power\n"
-expect "failed"
-expect "status 0x7"
-expect_line "Done"
+expect "Error 7: InvalidArgs"
 send "diag rcp power 10\n"
 expect_line "Done"
 send "diag rcp echo\n"
-expect "failed"
-expect "status 0x7"
-expect_line "Done"
+expect "Error 7: InvalidArgs"
 send "diag rcp echo 0123456789\n"
 expect_line "0123456789"
 expect_line "Done"
diff --git a/tests/scripts/expect/posix-power-calibration.exp b/tests/scripts/expect/posix-power-calibration.exp
new file mode 100755
index 0000000..a8f15fb
--- /dev/null
+++ b/tests/scripts/expect/posix-power-calibration.exp
@@ -0,0 +1,74 @@
+#!/usr/bin/expect -f
+#
+#  Copyright (c) 2022, 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.
+#
+
+source "tests/scripts/expect/_common.exp"
+
+spawn_node 1
+
+send "diag start\n"
+expect "start diagnostics mode"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag powersettings 11\n"
+expect "failed"
+expect "status 0x17"
+expect_line "Error 23: NotFound"
+
+send "diag powersettings\n"
+expect "| StartCh | EndCh | TargetPower | ActualPower | RawPowerSetting |"
+expect "+---------+-------+-------------+-------------+-----------------+"
+expect_line "Done"
+
+send "diag stop\n"
+expect_line "Done"
+
+send "region US\n"
+expect_line "Done"
+
+send "diag start\n"
+expect "start diagnostics mode"
+expect "status 0x00"
+expect_line "Done"
+
+send "diag powersettings 11\n"
+expect -re {TargetPower\(0\.01dBm\): -?\d+}
+expect -re {ActualPower\(0\.01dBm\): -?\d+}
+expect -re {RawPowerSetting: [0-9]{1,16}}
+expect_line "Done"
+
+send "diag powersettings\n"
+expect "| StartCh | EndCh | TargetPower | ActualPower | RawPowerSetting |"
+expect "+---------+-------+-------------+-------------+-----------------+"
+for {set i 1} {$i <= 4} {incr i} {
+    expect -re "\\| +\\d+ | +\\d+ | +\\d+ | +\\d+ | +\[0-9\]\{1,16\} |"
+}
+expect_line "Done"
+
+dispose_all
diff --git a/tests/scripts/expect/posix-rcp-restoration.exp b/tests/scripts/expect/posix-rcp-restoration.exp
index c0dac4c..1d3fac2 100755
--- a/tests/scripts/expect/posix-rcp-restoration.exp
+++ b/tests/scripts/expect/posix-rcp-restoration.exp
@@ -251,12 +251,7 @@
     puts "While energy scanning"
 
     spawn_node 1 "rcp" "spinel+hdlc_uart://$host_pty"
-    send "ifconfig up\n"
-    expect_line "Done"
-    send "thread start\n"
-    expect_line "Done"
-    wait_for "state" "leader"
-    expect_line "Done"
+    attach
 
     send "scan energy 100\n"
     expect "| Ch | RSSI |"
diff --git a/tests/scripts/expect/posix-scan-tx-to-sleep.exp b/tests/scripts/expect/posix-scan-tx-to-sleep.exp
index 2ac44a8..eea15b6 100755
--- a/tests/scripts/expect/posix-scan-tx-to-sleep.exp
+++ b/tests/scripts/expect/posix-scan-tx-to-sleep.exp
@@ -36,12 +36,7 @@
 expect_line "Done"
 send "dataset commit active\n"
 expect_line "Done"
-send "ifconfig up\n"
-expect_line "Done"
-send "thread start\n"
-expect_line "Done"
-wait_for "state" "leader"
-expect_line "Done"
+attach
 
 set extaddr [get_extaddr]
 send "panid\n"
diff --git a/.lgtm.yml b/tests/scripts/expect/tun-dns-over-tcp-client.exp
old mode 100644
new mode 100755
similarity index 79%
copy from .lgtm.yml
copy to tests/scripts/expect/tun-dns-over-tcp-client.exp
index 9051e95..3da428a
--- a/.lgtm.yml
+++ b/tests/scripts/expect/tun-dns-over-tcp-client.exp
@@ -1,3 +1,4 @@
+#!/usr/bin/expect -f
 #
 #  Copyright (c) 2020, The OpenThread Authors.
 #  All rights reserved.
@@ -26,12 +27,20 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-extraction:
-  cpp:
-    prepare:
-      packages:
-        - cmake
-        - ninja-build
-    index:
-      build_command:
-        - THREAD_VERSION=1.2 NODE_MODE=rcp OT_NATIVE_IP=1 ./script/test build
+source "tests/scripts/expect/_common.exp"
+source "tests/scripts/expect/_multinode.exp"
+
+spawn_node 2 "cli"
+spawn_node 1
+setup_leader
+setup_node 2 "rnd" "router"
+
+switch_node 1
+set addr_1 [get_ipaddr mleid]
+
+switch_node 2
+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"
+
+dispose_all
diff --git a/tests/scripts/expect/tun-realm-local-multicast.exp b/tests/scripts/expect/tun-realm-local-multicast.exp
index b18ffba..7be7c5e 100755
--- a/tests/scripts/expect/tun-realm-local-multicast.exp
+++ b/tests/scripts/expect/tun-realm-local-multicast.exp
@@ -39,27 +39,15 @@
 spawn_node 1
 setup_default_network
 
-send "ifconfig up\n"
-expect_line "Done"
-
-send "thread start\n"
-expect_line "Done"
-
-wait_for "state" "leader"
-expect_line "Done"
+attach
 
 set extaddr1 [get_extaddr]
 
 spawn_node 2 cli
 setup_default_network
 
-send "ifconfig up\n"
-expect_line "Done"
+attach "router"
 
-send "thread start\n"
-expect_line "Done"
-
-wait_for "state" "router"
 sleep 3
 
 spawn_node 3 cli
@@ -71,13 +59,8 @@
 send "macfilter addr denylist\n"
 expect_line "Done"
 
-send "ifconfig up\n"
-expect_line "Done"
+attach "router"
 
-send "thread start\n"
-expect_line "Done"
-
-wait_for "state" "router"
 sleep 4
 
 set extaddr3 [get_extaddr]
@@ -94,13 +77,7 @@
 send "macfilter addr allowlist\n"
 expect_line "Done"
 
-send "ifconfig up\n"
-expect_line "Done"
-
-send "thread start\n"
-expect_line "Done"
-
-wait_for "state" "child"
+attach "child"
 
 set mleid4 [get_ipaddr "mleid"]
 
diff --git a/tests/scripts/expect/tun-sntp.exp b/tests/scripts/expect/tun-sntp.exp
index a724390..fe91233 100755
--- a/tests/scripts/expect/tun-sntp.exp
+++ b/tests/scripts/expect/tun-sntp.exp
@@ -35,12 +35,7 @@
 
 send "panid 0xface\n"
 expect_line "Done"
-send "ifconfig up\n"
-expect_line "Done"
-send "thread start\n"
-expect_line "Done"
-wait_for "state" "leader"
-expect_line "Done"
+attach
 
 send "sntp query ::1 123\n"
 expect -re {SNTP response - Unix time: \d+ \(era: \d+\)}
diff --git a/tests/scripts/expect/tun-udp.exp b/tests/scripts/expect/tun-udp.exp
index 03ba12f..53cff14 100755
--- a/tests/scripts/expect/tun-udp.exp
+++ b/tests/scripts/expect/tun-udp.exp
@@ -38,14 +38,7 @@
 send "panid 0xface\n"
 expect_line "Done"
 
-send "ifconfig up\n"
-expect_line "Done"
-
-send "thread start\n"
-expect_line "Done"
-
-wait_for "state" "leader"
-expect_line "Done"
+attach
 
 send "udp open\n"
 expect_line "Done"
diff --git a/tests/scripts/thread-cert/Cert_5_1_01_RouterAttach.py b/tests/scripts/thread-cert/Cert_5_1_01_RouterAttach.py
index 1e4c326..0c87399 100755
--- a/tests/scripts/thread-cert/Cert_5_1_01_RouterAttach.py
+++ b/tests/scripts/thread-cert/Cert_5_1_01_RouterAttach.py
@@ -271,53 +271,8 @@
                    ).\
             must_next()
 
-        # Step 8: Router Sends a Link Request Message.
-        #         The Link Request Message MUST be multicast and contain
-        #         the following TLVs:
-        #             - Challenge TLV
-        #             - Leader Data TLV
-        #             - Source Address TLV
-        #             - Version TLV
-        #             - TLV Request TLV: Link Margin
-
-        pkts.filter_wpan_src64(ROUTER).\
-            filter_LLARMA().\
-            filter_mle_cmd(MLE_LINK_REQUEST).\
-            filter(lambda p: {
-                              CHALLENGE_TLV,
-                              LEADER_DATA_TLV,
-                              SOURCE_ADDRESS_TLV,
-                              VERSION_TLV,
-                              TLV_REQUEST_TLV,
-                              LINK_MARGIN_TLV
-                              } <= set(p.mle.tlv.type)\
-                   ).\
-            must_next()
-
-        # Step 9: Leader sends a Unicast Link Accept and Request Message.
-        #         The Message MUST be unicast to Router
-        #         The Message MUST contain the following TLVs:
-        #             - Leader Data TLV
-        #             - Link-layer Frame Counter TLV
-        #             - Link Margin TLV
-        #             - Response TLV
-        #             - Source Address TLV
-        #             - Version TLV
-        #             - Challenge TLV (optional)
-        #             - MLE Frame Counter TLV (optional)
-
-        pkts.filter_wpan_src64(LEADER).\
-            filter_wpan_dst64(ROUTER).\
-            filter_mle_cmd(MLE_LINK_ACCEPT_AND_REQUEST).\
-            filter(lambda p: {
-                              LEADER_DATA_TLV,
-                              LINK_LAYER_FRAME_COUNTER_TLV,
-                              LINK_MARGIN_TLV,
-                              RESPONSE_TLV,
-                              SOURCE_ADDRESS_TLV,
-                              VERSION_TLV
-                               } <= set(p.mle.tlv.type)).\
-                   must_next()
+        # Steps 8 and 9 are skipped due to change the Link establishment
+        # process (no multicast MLE Link Request by new router).
 
         # Step 10: Router is sending properly formatted MLE Advertisements.
         #          MLE Advertisements MUST be sent with an IP Hop Limit of
diff --git a/tests/scripts/thread-cert/Cert_5_2_04_REEDUpgrade.py b/tests/scripts/thread-cert/Cert_5_2_04_REEDUpgrade.py
index 241c9bb..5a05068 100755
--- a/tests/scripts/thread-cert/Cert_5_2_04_REEDUpgrade.py
+++ b/tests/scripts/thread-cert/Cert_5_2_04_REEDUpgrade.py
@@ -344,28 +344,8 @@
             must_next()
 
         # Step 10: REED Sends a Link Request Message.
-        #          The Link Request Message MUST be multicast and contain
-        #          the following TLVs:
-        #              - Challenge TLV
-        #              - Leader Data TLV
-        #              - Source Address TLV
-        #              - TLV Request TLV: Link Margin
-        #              - Version TLV
-
-        with pkts.save_index():
-            pkts.filter_wpan_src64(REED).\
-                filter_LLARMA().\
-                filter_mle_cmd(MLE_LINK_REQUEST).\
-                filter(lambda p: {
-                                 CHALLENGE_TLV,
-                                 LEADER_DATA_TLV,
-                                 SOURCE_ADDRESS_TLV,
-                                 VERSION_TLV,
-                                 TLV_REQUEST_TLV,
-                                 LINK_MARGIN_TLV
-                                 } <= set(p.mle.tlv.type)
-                       ).\
-                must_next()
+        # This step is skipped due to change that new router no
+        # longer send multicast Link Request.
 
         # Step 11: The REED MLE Child ID Response MUST be properly
         #          formatted with MED_1’s new 16-bit address.
diff --git a/tests/scripts/thread-cert/Cert_5_3_06_RouterIdMask.py b/tests/scripts/thread-cert/Cert_5_3_06_RouterIdMask.py
index 1f57e9f..97455af 100755
--- a/tests/scripts/thread-cert/Cert_5_3_06_RouterIdMask.py
+++ b/tests/scripts/thread-cert/Cert_5_3_06_RouterIdMask.py
@@ -114,7 +114,7 @@
         # 5
 
         self.nodes[ROUTER2].start()
-        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.simulator.go(config.ROUTER_RESET_DELAY)
         self.assertEqual(self.nodes[ROUTER2].get_state(), 'router')
 
         self.simulator.go(config.MAX_ADVERTISEMENT_INTERVAL)
@@ -190,12 +190,8 @@
                 filter_mle_cmd(MLE_ADVERTISEMENT).\
                 filter(lambda p: p.sniff_timestamp - _pkt.sniff_timestamp <= 4).\
                 must_next()
-        # check router cost before and after the re-attach
-        pkts.filter_wpan_src64(LEADER).\
-            filter_LLANMA().\
-            filter_mle_cmd(MLE_ADVERTISEMENT).\
-            filter(lambda p: {1,0,1} == set(p.mle.tlv.route64.cost)).\
-            must_next()
+
+        # check router cost after the re-attach
         pkts.filter_wpan_src64(LEADER).\
             filter_LLANMA().\
             filter_mle_cmd(MLE_ADVERTISEMENT).\
diff --git a/tests/scripts/thread-cert/Cert_5_5_02_LeaderReboot.py b/tests/scripts/thread-cert/Cert_5_5_02_LeaderReboot.py
index 6aff3cf..80aba87 100755
--- a/tests/scripts/thread-cert/Cert_5_5_02_LeaderReboot.py
+++ b/tests/scripts/thread-cert/Cert_5_5_02_LeaderReboot.py
@@ -84,7 +84,7 @@
         self.assertEqual(self.nodes[ROUTER].get_state(), 'leader')
 
         self.nodes[LEADER].start()
-        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.simulator.go(config.LEADER_RESET_DELAY)
         self.assertEqual(self.nodes[LEADER].get_state(), 'router')
 
         addrs = self.nodes[ED].get_addrs()
@@ -176,14 +176,9 @@
             thread_address.tlv.status == 0)
 
         #Step 15: Leader Send a Multicast Link Request
-        _lpkts.filter_mle_cmd(MLE_LINK_REQUEST).must_next().must_verify(
-            lambda p: {VERSION_TLV, TLV_REQUEST_TLV, SOURCE_ADDRESS_TLV, LEADER_DATA_TLV, CHALLENGE_TLV} < set(
-                p.mle.tlv.type))
-
         #Step 16: Router_1 send a Unicast Link Accept
-        _rpkts.filter_mle_cmd(MLE_LINK_ACCEPT_AND_REQUEST).must_next().must_verify(lambda p: {
-            VERSION_TLV, SOURCE_ADDRESS_TLV, RESPONSE_TLV, MLE_FRAME_COUNTER_TLV, LINK_MARGIN_TLV, LEADER_DATA_TLV
-        } < set(p.mle.tlv.type))
+        # Steps 15 and 16 are skipped since new router no longer
+        # send multicast Link Request.
 
         #Step 17: Router_1 MUST respond with an ICMPv6 Echo Reply
         _rpkts.filter_ping_request().filter_wpan_dst64(MED).must_next()
diff --git a/tests/scripts/thread-cert/Cert_5_5_03_SplitMergeChildren.py b/tests/scripts/thread-cert/Cert_5_5_03_SplitMergeChildren.py
index 92040d0..06f77f6 100755
--- a/tests/scripts/thread-cert/Cert_5_5_03_SplitMergeChildren.py
+++ b/tests/scripts/thread-cert/Cert_5_5_03_SplitMergeChildren.py
@@ -114,7 +114,7 @@
         self.assertEqual(self.nodes[ROUTER2].get_state(), 'leader')
 
         self.nodes[LEADER].start()
-        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.simulator.go(config.LEADER_RESET_DELAY)
         self.assertEqual(self.nodes[LEADER].get_state(), 'router')
 
         self.simulator.go(30)
diff --git a/tests/scripts/thread-cert/Cert_5_5_04_SplitMergeRouters.py b/tests/scripts/thread-cert/Cert_5_5_04_SplitMergeRouters.py
index 8c08d1e..2bcc876 100755
--- a/tests/scripts/thread-cert/Cert_5_5_04_SplitMergeRouters.py
+++ b/tests/scripts/thread-cert/Cert_5_5_04_SplitMergeRouters.py
@@ -102,7 +102,7 @@
         self.simulator.go(150)
 
         self.nodes[LEADER].start()
-        self.simulator.go(50)
+        self.simulator.go(50 + config.LEADER_RESET_DELAY)
 
         self.assertEqual(self.nodes[LEADER].get_state(), 'router')
 
diff --git a/tests/scripts/thread-cert/Cert_5_5_05_SplitMergeREED.py b/tests/scripts/thread-cert/Cert_5_5_05_SplitMergeREED.py
index abfab51..0cd99f5 100755
--- a/tests/scripts/thread-cert/Cert_5_5_05_SplitMergeREED.py
+++ b/tests/scripts/thread-cert/Cert_5_5_05_SplitMergeREED.py
@@ -197,9 +197,8 @@
             lambda p: {NL_MAC_EXTENDED_ADDRESS_TLV, NL_STATUS_TLV} <= set(p.coap.tlv.type))
 
         # Step 7: DUT send a Multicast Link Request
-        pkts.filter_wpan_src64(REED).filter_mle_cmd(MLE_LINK_REQUEST).must_next().must_verify(lambda p: {
-            VERSION_TLV, TLV_REQUEST_TLV, SOURCE_ADDRESS_TLV, LEADER_DATA_TLV, CHALLENGE_TLV, LINK_MARGIN_TLV
-        } <= set(p.mle.tlv.type))
+        # Step 7 is skipped since new router no longer sends
+        # multicast Link Request.
 
         # Step 8: DUT send Child ID Response to Router_1
         reed_pkts.filter_mle_cmd(MLE_CHILD_ID_RESPONSE).must_next().must_verify(lambda p: p.wpan.dst64 == ROUTER_1)
diff --git a/tests/scripts/thread-cert/Cert_5_5_07_SplitMergeThreeWay.py b/tests/scripts/thread-cert/Cert_5_5_07_SplitMergeThreeWay.py
index 3b6e201..fbb9dee 100755
--- a/tests/scripts/thread-cert/Cert_5_5_07_SplitMergeThreeWay.py
+++ b/tests/scripts/thread-cert/Cert_5_5_07_SplitMergeThreeWay.py
@@ -93,7 +93,7 @@
         self.simulator.go(140)
 
         self.nodes[LEADER1].start()
-        self.simulator.go(30)
+        self.simulator.go(30 + config.LEADER_RESET_DELAY)
 
         addrs = self.nodes[LEADER1].get_addrs()
         for addr in addrs:
diff --git a/tests/scripts/thread-cert/Cert_7_1_06_BorderRouterAsLeader.py b/tests/scripts/thread-cert/Cert_7_1_06_BorderRouterAsLeader.py
index f6053cb..36e0f9c 100755
--- a/tests/scripts/thread-cert/Cert_7_1_06_BorderRouterAsLeader.py
+++ b/tests/scripts/thread-cert/Cert_7_1_06_BorderRouterAsLeader.py
@@ -143,7 +143,7 @@
         self.simulator.go(720)
 
         self.nodes[ROUTER_1].start()
-        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.simulator.go(config.ROUTER_RESET_DELAY)
         self.assertEqual(self.nodes[ROUTER_1].get_state(), 'router')
         self.collect_rloc16s()
 
diff --git a/tests/scripts/thread-cert/Cert_7_1_07_BorderRouterAsLeader.py b/tests/scripts/thread-cert/Cert_7_1_07_BorderRouterAsLeader.py
index 617fb58..49e000b 100755
--- a/tests/scripts/thread-cert/Cert_7_1_07_BorderRouterAsLeader.py
+++ b/tests/scripts/thread-cert/Cert_7_1_07_BorderRouterAsLeader.py
@@ -158,7 +158,7 @@
 
         # Wait for Router_2 reattachment and network data propagation
         # ADVERTISEMENT_I_MAX + DEFAULT_CHILD_TIMEOUT + ATTACH_DELAY + Extra
-        self.simulator.go(60)
+        self.simulator.go(120)
         self.assertEqual(self.nodes[ROUTER_2].get_state(), 'router')
         self.collect_ipaddrs()
         self.collect_rloc16s()
diff --git a/tests/scripts/thread-cert/Cert_9_2_15_PendingPartition.py b/tests/scripts/thread-cert/Cert_9_2_15_PendingPartition.py
index 13d3e2b..a0e2bcc 100755
--- a/tests/scripts/thread-cert/Cert_9_2_15_PendingPartition.py
+++ b/tests/scripts/thread-cert/Cert_9_2_15_PendingPartition.py
@@ -138,7 +138,7 @@
         self.simulator.go(100)
 
         self.nodes[ROUTER2].start()
-        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.simulator.go(config.ROUTER_RESET_DELAY)
         self.assertEqual(self.nodes[ROUTER2].get_state(), 'router')
         self.simulator.go(100)
 
diff --git a/tests/scripts/thread-cert/Cert_9_2_16_ActivePendingPartition.py b/tests/scripts/thread-cert/Cert_9_2_16_ActivePendingPartition.py
index e257f28..8515659 100755
--- a/tests/scripts/thread-cert/Cert_9_2_16_ActivePendingPartition.py
+++ b/tests/scripts/thread-cert/Cert_9_2_16_ActivePendingPartition.py
@@ -142,7 +142,7 @@
         self.simulator.go(100)
 
         self.nodes[ROUTER2].start()
-        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.simulator.go(config.ROUTER_RESET_DELAY)
         self.assertEqual(self.nodes[ROUTER2].get_state(), 'router')
 
         self.assertEqual(self.nodes[COMMISSIONER].get_network_name(), NETWORK_NAME_FINAL)
diff --git a/tests/scripts/thread-cert/Makefile.am b/tests/scripts/thread-cert/Makefile.am
deleted file mode 100644
index d6e4210..0000000
--- a/tests/scripts/thread-cert/Makefile.am
+++ /dev/null
@@ -1,399 +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_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_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_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_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
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
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
index 8da0c1c..cf7405e
--- 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
@@ -143,13 +143,25 @@
         self.collect_ipaddrs()
         self.collect_rloc16s()
 
-        # ping MA1 from Host could get replied from R1 and R2
+        # ping MA1 from Host could generate a reply from R1 and R2
         self.assertTrue(self.nodes[HOST].ping(MA1, backbone=True, ttl=5))
         self.simulator.go(WAIT_REDUNDANCE)
+        self.verify_border_routing_counters(self.nodes[PBBR1], {'inbound_multicast': 1, 'outbound_unicast': 1})
+        self.verify_border_routing_counters(self.nodes[PBBR2], {'inbound_multicast': 1, 'outbound_unicast': 1})
 
-        # ping MA2 from R1 could get replied from Host and R2
+        # ping MA2 from R1 could generate a reply from Host and R2
         self.assertTrue(self.nodes[ROUTER1].ping(MA2))
         self.simulator.go(WAIT_REDUNDANCE)
+        self.verify_border_routing_counters(self.nodes[PBBR1], {'inbound_unicast': 2, 'outbound_multicast': 1})
+        self.verify_border_routing_counters(self.nodes[PBBR2], {'inbound_multicast': 1, 'outbound_unicast': 1})
+
+        # ping MA2 from R1's MLE-ID shouldn't generate a reply from Host or R2
+        self.assertFalse(self.nodes[ROUTER1].ping(MA2, interface=self.nodes[ROUTER1].get_mleid()))
+        self.simulator.go(WAIT_REDUNDANCE)
+
+        # ping MA2 from R1's LLA shouldn't generate a reply from Host or R2
+        self.assertFalse(self.nodes[ROUTER1].ping(MA2, interface=self.nodes[ROUTER1].get_linklocal()))
+        self.simulator.go(WAIT_REDUNDANCE)
 
     def verify(self, pv: PacketVerifier):
         pkts = pv.pkts
@@ -211,6 +223,35 @@
         pkts.filter_wpan_src64(ROUTER2).filter_ipv6_src_dst(
             ROUTER2_DUA, ROUTER1_DUA).filter_ping_reply(identifier=ping_ma2.icmpv6.echo.identifier).must_next()
 
+        #
+        # Verify pinging MA2 from R1's MLE-ID will not be forwarded to the Backbone link
+        #
+
+        # ROUTER1 should send the multicast ping request
+        ping_ma2_2 = pkts.filter_wpan_src64(ROUTER1).filter_AMPLFMA(mpl_seed_id=ROUTER1_RLOC16).filter_ping_request(
+            identifier=ping_ma2.icmpv6.echo.identifier + 1).must_next()
+
+        # PBBR1 shouldn't forward the multicast ping request to the Backbone link
+        pkts.filter_eth_src(PBBR1_ETH).filter_ping_request(ping_ma2_2.icmpv6.echo.identifier).must_not_next()
+
+        #
+        # Verify pinging MA2 from R1's Link-Local address will not be forwarded to the Backbone link
+        #
+
+        # ROUTER1 should send the multicast ping request
+        ping_ma2_3 = pkts.filter_wpan_src64(ROUTER1).filter_AMPLFMA(mpl_seed_id=ROUTER1_RLOC16).filter_ping_request(
+            identifier=ping_ma2.icmpv6.echo.identifier + 1).must_next()
+
+        # PBBR1 shouldn't forward the multicast ping request to the Backbone link
+        pkts.filter_eth_src(PBBR1_ETH).filter_ping_request(ping_ma2_3.icmpv6.echo.identifier).must_not_next()
+
+    def verify_border_routing_counters(self, br, expect_delta):
+        delta_counters = br.read_border_routing_counters_delta()
+        self.assertEqual(set(delta_counters.keys()), set(expect_delta.keys()))
+        for key in delta_counters:
+            self.assertGreaterEqual(delta_counters[key][0], expect_delta[key])
+            self.assertGreater(delta_counters[key][1], 0)
+
 
 if __name__ == '__main__':
     unittest.main()
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 8ce9e0d..7a4791d
--- 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
@@ -30,14 +30,12 @@
 
 import config
 import thread_cert
+import enum
 
 # Test description:
-#   This test verifies that a single NAT64 prefix is advertised when there are
+#   This test verifies that a single NAT64 prefix is published when there are
 #   multiple Border Routers in the same Thread and infrastructure network.
 #
-#   TODO: add checks for outbound connectivity from Thread device to IPv4 host
-#         after OTBR change is ready.
-#
 # Topology:
 #    ----------------(eth)--------------------------
 #           |                 |             |
@@ -51,6 +49,13 @@
 BR2 = 3
 HOST = 4
 
+NAT64_PREFIX_REFRESH_DELAY = 305
+
+NAT64_STATE_DISABLED = 'disabled'
+NAT64_STATE_NOT_RUNNING = 'not_running'
+NAT64_STATE_IDLE = 'idle'
+NAT64_STATE_ACTIVE = 'active'
+
 
 class Nat64MultiBorderRouter(thread_cert.TestCase):
     USE_MESSAGE_FACTORY = False
@@ -89,7 +94,12 @@
         self.simulator.go(5)
 
         br1.start()
+        # When feature flag is enabled, NAT64 might be disabled by default. So
+        # ensure NAT64 is enabled here.
+        br1.nat64_set_enabled(True)
         self.simulator.go(config.LEADER_STARTUP_DELAY)
+        br1.bash("service bind9 stop")
+        self.simulator.go(NAT64_PREFIX_REFRESH_DELAY)
         self.assertEqual('leader', br1.get_state())
 
         router.start()
@@ -97,44 +107,156 @@
         self.assertEqual('router', router.get_state())
 
         #
-        # Case 1. BR2 joins the network later and it will not add
-        #         its local nat64 prefix to Network Data.
+        # Case 1. BR2 with an infrastructure prefix joins the network later and
+        #         it will add the infrastructure nat64 prefix to Network Data.
+        #         Note: NAT64 translator will be bypassed.
         #
         br2.start()
+        # When feature flag is enabled, NAT64 might be disabled by default. So
+        # ensure NAT64 is enabled here.
+        br2.nat64_set_enabled(True)
         self.simulator.go(config.BORDER_ROUTER_STARTUP_DELAY)
         self.assertEqual('router', br2.get_state())
 
-        # Only 1 NAT64 prefix in Network Data.
-        self.simulator.go(30)
+        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_infra_nat64_prefix = br2.get_br_favored_nat64_prefix()
+
         self.assertEqual(len(br1.get_netdata_nat64_prefix()), 1)
-        self.assertEqual(len(br2.get_netdata_nat64_prefix()), 1)
-        self.assertEqual(br1.get_netdata_nat64_prefix()[0], br2.get_netdata_nat64_prefix()[0])
         nat64_prefix = br1.get_netdata_nat64_prefix()[0]
-
-        # The NAT64 prefix in Network Data is same as BR1's local NAT64 prefix.
-        br1_nat64_prefix = br1.get_br_nat64_prefix()
-        br2_nat64_prefix = br2.get_br_nat64_prefix()
-        self.assertEqual(nat64_prefix, br1_nat64_prefix)
-        self.assertNotEqual(nat64_prefix, br2_nat64_prefix)
+        self.assertEqual(nat64_prefix, br2_infra_nat64_prefix)
+        self.assertNotEqual(nat64_prefix, br1_local_nat64_prefix)
+        self.assertDictIncludes(br1.nat64_state, {
+            'PrefixManager': NAT64_STATE_IDLE,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
+        self.assertDictIncludes(br2.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
 
         #
-        # Case 2. Disable and re-enable border routing on BR1.
+        # Case 2. Disable NAT64 on BR2.
+        #         BR1 will add its local nat64 prefix.
         #
-        br1.disable_br()
-        self.simulator.go(30)
+        br2.nat64_set_enabled(False)
+        self.simulator.go(10)
 
-        # BR1 withdraws its prefix and BR2 advertises its prefix.
         self.assertEqual(len(br1.get_netdata_nat64_prefix()), 1)
-        self.assertEqual(br2_nat64_prefix, br1.get_netdata_nat64_prefix()[0])
-        self.assertNotEqual(br1_nat64_prefix, br1.get_netdata_nat64_prefix()[0])
+        nat64_prefix = br1.get_netdata_nat64_prefix()[0]
+        self.assertEqual(nat64_prefix, br1_local_nat64_prefix)
+        self.assertDictIncludes(br1.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_ACTIVE
+        })
+        self.assertDictIncludes(br2.nat64_state, {
+            'PrefixManager': NAT64_STATE_DISABLED,
+            'Translator': NAT64_STATE_DISABLED
+        })
 
-        br1.enable_br()
-        self.simulator.go(config.BORDER_ROUTER_STARTUP_DELAY)
+        #
+        # Case 3. Re-enables BR2 with a local prefix and it will not add
+        #         its local nat64 prefix to Network Data.
+        #
+        br2.bash("service bind9 stop")
+        self.simulator.go(5)
+        br2.nat64_set_enabled(True)
 
-        # NAT64 prefix in Network Data is still advertised by BR2.
+        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(len(br1.get_netdata_nat64_prefix()), 1)
-        self.assertEqual(br2_nat64_prefix, br1.get_netdata_nat64_prefix()[0])
-        self.assertNotEqual(br1_nat64_prefix, br1.get_netdata_nat64_prefix()[0])
+        nat64_prefix = br1.get_netdata_nat64_prefix()[0]
+        self.assertEqual(nat64_prefix, br1_local_nat64_prefix)
+        self.assertNotEqual(nat64_prefix, br2_local_nat64_prefix)
+        self.assertDictIncludes(br1.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_ACTIVE
+        })
+        self.assertDictIncludes(br2.nat64_state, {
+            'PrefixManager': NAT64_STATE_IDLE,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
+
+        #
+        # Case 4. Disable NAT64 on BR1.
+        #         BR1 withdraws its local prefix and BR2 advertises its local prefix.
+        #
+        br1.nat64_set_enabled(False)
+
+        self.simulator.go(10)
+        self.assertEqual(len(br1.get_netdata_nat64_prefix()), 1)
+        nat64_prefix = br1.get_netdata_nat64_prefix()[0]
+        self.assertEqual(br2_local_nat64_prefix, nat64_prefix)
+        self.assertNotEqual(br1_local_nat64_prefix, nat64_prefix)
+        self.assertDictIncludes(br1.nat64_state, {
+            'PrefixManager': NAT64_STATE_DISABLED,
+            'Translator': NAT64_STATE_DISABLED
+        })
+        self.assertDictIncludes(br2.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_ACTIVE
+        })
+
+        #
+        # Case 5. Re-enable NAT64 on BR1.
+        #         NAT64 prefix in Network Data is still BR2's local prefix.
+        #
+        br1.nat64_set_enabled(True)
+
+        self.simulator.go(10)
+        self.assertEqual(len(br1.get_netdata_nat64_prefix()), 1)
+        nat64_prefix = br1.get_netdata_nat64_prefix()[0]
+        self.assertEqual(br2_local_nat64_prefix, nat64_prefix)
+        self.assertNotEqual(br1_local_nat64_prefix, nat64_prefix)
+        self.assertDictIncludes(br1.nat64_state, {
+            'PrefixManager': NAT64_STATE_IDLE,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
+        self.assertDictIncludes(br2.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_ACTIVE
+        })
+
+        #
+        # Case 6. Disable the routing manager should stop NAT64 prefix manager.
+        #
+        #
+        br2.disable_br()
+        self.simulator.go(10)
+        self.assertEqual(len(br1.get_netdata_nat64_prefix()), 1)
+        nat64_prefix = br1.get_netdata_nat64_prefix()[0]
+        self.assertEqual(br1_local_nat64_prefix, nat64_prefix)
+        self.assertNotEqual(br2_local_nat64_prefix, nat64_prefix)
+        self.assertDictIncludes(br1.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_ACTIVE
+        })
+        self.assertDictIncludes(br2.nat64_state, {
+            'PrefixManager': NAT64_STATE_NOT_RUNNING,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
+
+        #
+        # Case 7. Enable the routing manager the BR should start NAT64 prefix manager if the prefix manager is enabled.
+        #
+        #
+        br2.enable_br()
+        self.simulator.go(10)
+        self.assertEqual(len(br1.get_netdata_nat64_prefix()), 1)
+        nat64_prefix = br1.get_netdata_nat64_prefix()[0]
+        self.assertEqual(br1_local_nat64_prefix, nat64_prefix)
+        self.assertNotEqual(br2_local_nat64_prefix, nat64_prefix)
+        self.assertDictIncludes(br1.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_ACTIVE
+        })
+        self.assertDictIncludes(br2.nat64_state, {
+            'PrefixManager': NAT64_STATE_IDLE,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
 
 
 if __name__ == '__main__':
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 a301cda..4a69b2a
--- 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
@@ -30,12 +30,19 @@
 
 import config
 import thread_cert
+import ipv6
+
+import ipaddress
+
+# For NAT64 connectivity tests
+import socket
+import select
 
 # Test description:
-#   This test verifies the advertisement of NAT64 prefix in Thread network.
-#
-#   TODO: add checks for outbound connectivity from Thread device to IPv4 host
-#         after OTBR change is ready.
+#   This test verifies publishing the local NAT64 prefix in Thread network
+#   when no NAT64 prefix found on infrastructure interface.
+#   It also verifies the outbound connectivity from a Thread device to
+#   an IPv4 host address.
 #
 # Topology:
 #    ----------------(eth)--------------------
@@ -49,9 +56,12 @@
 ROUTER = 2
 HOST = 3
 
-# The prefix is set small enough that a random-generated NAT64 prefix is very
-# likely greater than it. So that the BR will remove the random-generated one.
+# The prefix is set small enough that a random-generated ULA NAT64 prefix is very
+# likely greater than it. So the BR will remove the random-generated one.
 SMALL_NAT64_PREFIX = "fd00:00:00:01:00:00::/96"
+# The prefix is set larger than a random-generated ULA NAT64 prefix.
+# So the BR will remove the random-generated one.
+LARGE_NAT64_PREFIX = "ff00:00:00:01:00:00::/96"
 
 
 class Nat64SingleBorderRouter(thread_cert.TestCase):
@@ -75,6 +85,28 @@
         },
     }
 
+    def get_host_ip(self):
+        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        sock.setblocking(False)
+        # Note: The address is not important, we use this function to get the host ip address.
+        sock.connect(('8.8.8.8', 54321))
+        host_ip, _ = sock.getsockname()
+        sock.close()
+        return host_ip
+
+    def receive_from(self, sock, timeout_seconds):
+        ready = select.select([sock], [], [], timeout_seconds)
+        if ready[0]:
+            return sock.recv(1024)
+        else:
+            raise AssertionError("No data recevied")
+
+    def listen_udp(self, addr, port):
+        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        sock.setblocking(False)
+        sock.bind((addr, port))
+        return sock
+
     def test(self):
         br = self.nodes[BR]
         router = self.nodes[ROUTER]
@@ -84,7 +116,12 @@
         self.simulator.go(5)
 
         br.start()
+        # When feature flag is enabled, NAT64 might be disabled by default. So
+        # ensure NAT64 is enabled here.
+        br.nat64_set_enabled(True)
         self.simulator.go(config.LEADER_STARTUP_DELAY)
+        br.bash("service bind9 stop")
+        self.simulator.go(330)
         self.assertEqual('leader', br.get_state())
 
         router.start()
@@ -103,10 +140,10 @@
 
         #
         # Case 2.
-        # User adds a smaller NAT64 prefix and the local prefix is withdrawn.
+        # User adds a smaller NAT64 prefix (same preference) and the local prefix is withdrawn.
         # User removes the smaller NAT64 prefix and the local prefix is re-added.
         #
-        br.add_route(SMALL_NAT64_PREFIX, stable=False, nat64=True)
+        br.add_route(SMALL_NAT64_PREFIX, stable=False, nat64=True, prf='low')
         br.register_netdata()
         self.simulator.go(5)
 
@@ -115,13 +152,32 @@
 
         br.remove_route(SMALL_NAT64_PREFIX)
         br.register_netdata()
-        self.simulator.go(5)
+        self.simulator.go(10)
 
         self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
         self.assertEqual(local_nat64_prefix, br.get_netdata_nat64_prefix()[0])
 
         #
-        # Case 3. Disable and re-enable border routing on the border router.
+        # Case 3.
+        # User adds a larger NAT64 prefix (higher preference) and the local prefix is withdrawn.
+        # User removes the larger NAT64 prefix and the local prefix is re-added.
+        #
+        br.add_route(LARGE_NAT64_PREFIX, stable=False, nat64=True, prf='med')
+        br.register_netdata()
+        self.simulator.go(5)
+
+        self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
+        self.assertNotEqual(local_nat64_prefix, br.get_netdata_nat64_prefix()[0])
+
+        br.remove_route(LARGE_NAT64_PREFIX)
+        br.register_netdata()
+        self.simulator.go(10)
+
+        self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
+        self.assertEqual(local_nat64_prefix, br.get_netdata_nat64_prefix()[0])
+
+        #
+        # Case 4. Disable and re-enable border routing on the border router.
         #
         br.disable_br()
         self.simulator.go(5)
@@ -132,12 +188,57 @@
         br.enable_br()
         self.simulator.go(config.BORDER_ROUTER_STARTUP_DELAY)
 
-        # Same NAT64 prefix is advertised to Network Data.
-        self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
-        self.assertEqual(nat64_prefix, br.get_netdata_nat64_prefix()[0])
+        #
+        # Case 5. NAT64 connectivity and counters.
+        #
+        host_ip = self.get_host_ip()
+        self.assertTrue(router.ping(ipaddr=host_ip))
+
+        mappings = br.nat64_mappings
+        self.assertEqual(mappings[0]['counters']['ICMP']['4to6']['packets'], 1)
+        self.assertEqual(mappings[0]['counters']['ICMP']['6to4']['packets'], 1)
+        self.assertEqual(mappings[0]['counters']['total']['4to6']['packets'], 1)
+        self.assertEqual(mappings[0]['counters']['total']['6to4']['packets'], 1)
+
+        counters = br.nat64_counters
+        self.assertEqual(counters['protocol']['ICMP']['4to6']['packets'], 1)
+        self.assertEqual(counters['protocol']['ICMP']['6to4']['packets'], 1)
+
+        sock = self.listen_udp('0.0.0.0', 54321)
+        router.udp_start('::', 54321)
+        # We can use IPv4 addresses for commands like UDP send.
+        # The address will be converted to an IPv6 address by CLI.
+        router.udp_send(10, host_ip, 54321)
+        self.assertTrue(len(self.receive_from(sock, timeout_seconds=1)) == 10)
+
+        sock.close()
+
+        counters = br.nat64_counters
+        self.assertEqual(counters['protocol']['UDP']['6to4']['packets'], 1)
+        mappings = br.nat64_mappings
+        self.assertEqual(mappings[0]['counters']['UDP']['6to4']['packets'], 1)
+
+        # We should be able to get a IPv4 mapped IPv6 address.
+        # 203.0.113.1, RFC5737 TEST-NET-3, should be unreachable.
+        mapped_ip6_address = str(
+            ipv6.synthesize_ip6_address(ipaddress.IPv6Network(nat64_prefix), ipaddress.IPv4Address('203.0.113.1')))
+        self.assertFalse(router.ping(ipaddr=mapped_ip6_address))
+
+        mappings = br.nat64_mappings
+        self.assertEqual(mappings[0]['counters']['ICMP']['4to6']['packets'], 1)
+        self.assertEqual(mappings[0]['counters']['ICMP']['6to4']['packets'], 2)
+        self.assertEqual(mappings[0]['counters']['total']['4to6']['packets'], 1)
+        self.assertEqual(mappings[0]['counters']['total']['6to4']['packets'], 3)
+
+        counters = br.nat64_counters
+        self.assertEqual(counters['protocol']['ICMP']['4to6']['packets'], 1)
+        self.assertEqual(counters['protocol']['ICMP']['6to4']['packets'], 2)
 
         #
-        # Case 4. Disable and re-enable ethernet on the border router.
+        # Case 6. Disable and re-enable ethernet on the border router.
+        # Note: disable_ether will remove default route but enable_ether won't add it back,
+        # NAT64 connectivity tests will fail after this.
+        # TODO: Add a default IPv4 route after enable_ether.
         #
         br.disable_ether()
         self.simulator.go(5)
@@ -151,6 +252,9 @@
         # Same NAT64 prefix is advertised to Network Data.
         self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
         self.assertEqual(nat64_prefix, br.get_netdata_nat64_prefix()[0])
+        # Same NAT64 prefix is advertised to Network Data.
+        self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
+        self.assertEqual(nat64_prefix, br.get_netdata_nat64_prefix()[0])
 
 
 if __name__ == '__main__':
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
new file mode 100755
index 0000000..617ea0f
--- /dev/null
+++ b/tests/scripts/thread-cert/border_router/nat64/test_upstream_dns.py
@@ -0,0 +1,135 @@
+#!/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.
+#
+import unittest
+
+import config
+import thread_cert
+
+import ipaddress
+import shlex
+
+# Test description:
+#   This test verifies forwarding DNS queries sent by 'Router' by using
+# a record resolved by BIND9 server.
+#
+# Topology:
+#    ----------------(eth)--------------------
+#           |                 |
+#          BR (Leader)      HOST
+#           |
+#        ROUTER
+#
+
+BR = 1
+ROUTER = 2
+HOST = 3
+
+TEST_DOMAIN = 'test.domain'
+TEST_DOMAIN_IP6_ADDRESSES = {'2001:db8::1'}
+
+TEST_DOMAIN_BIND_CONF = f'''
+zone "{TEST_DOMAIN}" {{ type master; file "/etc/bind/db.test.domain"; }};
+'''
+
+TEST_DOMAIN_BIND_ZONE = f'''
+$TTL 24h
+@ IN SOA {TEST_DOMAIN} test.{TEST_DOMAIN}. ( 20230330 86400 300 604800 3600 )
+@ IN NS {TEST_DOMAIN}.
+''' + '\n'.join(f'@ IN AAAA {addr}' for addr in TEST_DOMAIN_IP6_ADDRESSES)
+
+
+class UpstreamDns(thread_cert.TestCase):
+    USE_MESSAGE_FACTORY = False
+
+    TOPOLOGY = {
+        BR: {
+            'name': 'BR',
+            'allowlist': [ROUTER],
+            'is_otbr': True,
+            'version': '1.3',
+        },
+        ROUTER: {
+            'name': 'Router',
+            'allowlist': [BR],
+            'version': '1.3',
+        },
+        HOST: {
+            'name': 'Host',
+            'is_host': True
+        },
+    }
+
+    def test(self):
+        br = self.nodes[BR]
+        router = self.nodes[ROUTER]
+        host = self.nodes[HOST]
+
+        host.start(start_radvd=False)
+        self.simulator.go(5)
+
+        br.start()
+        # When feature flag is enabled, NAT64 might be disabled by default. So
+        # ensure NAT64 is enabled here.
+        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.assertEqual('leader', br.get_state())
+
+        br.nat64_set_enabled(True)
+        br.srp_server_set_enabled(True)
+
+        br.bash('service bind9 stop')
+
+        br.bash(shlex.join(['echo', TEST_DOMAIN_BIND_CONF]) + ' >> /etc/bind/named.conf.local')
+        br.bash(shlex.join(['echo', TEST_DOMAIN_BIND_ZONE]) + ' >> /etc/bind/db.test.domain')
+
+        br.bash('service bind9 start')
+
+        router.start()
+        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.assertEqual('router', router.get_state())
+
+        self.simulator.go(10)
+        router.srp_client_enable_auto_start_mode()
+
+        # verify the server can forward the DNS query to upstream server.
+        self._verify_upstream_dns(br, router)
+
+    def _verify_upstream_dns(self, br, ed):
+        upstream_dns_enabled = br.dns_upstream_query_state
+        if not upstream_dns_enabled:
+            br.dns_upstream_query_state = True
+        self.assertTrue(br.dns_upstream_query_state)
+
+        resolved_names = ed.dns_resolve(TEST_DOMAIN)
+        self.assertEqual(len(resolved_names), len(TEST_DOMAIN_IP6_ADDRESSES))
+        for record in resolved_names:
+            self.assertIn(ipaddress.IPv6Address(record[0]).compressed, TEST_DOMAIN_IP6_ADDRESSES)
+
+
+if __name__ == '__main__':
+    unittest.main()
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
new file mode 100755
index 0000000..465b5a7
--- /dev/null
+++ b/tests/scripts/thread-cert/border_router/nat64/test_with_infrastructure_prefix.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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.
+#
+import unittest
+
+import config
+import thread_cert
+
+# Test description:
+#   This test verifies publishing infrastructure NAT64 prefix in Thread network.
+#
+#
+# Topology:
+#
+#   ----------------(eth)--------------------
+#           |
+#          BR (with DNS64 on infrastructure interface)
+#           |
+#        ROUTER
+#
+
+BR = 1
+ROUTER = 2
+
+# The prefix is set smaller than the default infrastructure NAT64 prefix.
+SMALL_NAT64_PREFIX = "2000:0:0:1:0:0::/96"
+
+NAT64_PREFIX_REFRESH_DELAY = 305
+
+NAT64_STATE_DISABLED = 'disabled'
+NAT64_STATE_NOT_RUNNING = 'not_running'
+NAT64_STATE_IDLE = 'idle'
+NAT64_STATE_ACTIVE = 'active'
+
+
+class Nat64SingleBorderRouter(thread_cert.TestCase):
+    USE_MESSAGE_FACTORY = False
+
+    TOPOLOGY = {
+        BR: {
+            'name': 'BR',
+            'allowlist': [ROUTER],
+            'is_otbr': True,
+            'version': '1.2',
+        },
+        ROUTER: {
+            'name': 'Router',
+            'allowlist': [BR],
+            'version': '1.2',
+        },
+    }
+
+    def test(self):
+        br = self.nodes[BR]
+        router = self.nodes[ROUTER]
+
+        br.start()
+        # When feature flag is enabled, NAT64 might be disabled by default. So
+        # ensure NAT64 is enabled here.
+        br.nat64_set_enabled(True)
+        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.assertEqual('leader', br.get_state())
+
+        router.start()
+        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()
+
+        self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
+        nat64_prefix = br.get_netdata_nat64_prefix()[0]
+        self.assertEqual(nat64_prefix, infra_nat64_prefix)
+        self.assertDictIncludes(br.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
+
+        # Case 2 Withdraw 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()
+        self.simulator.go(5)
+
+        self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
+        self.assertNotEqual(infra_nat64_prefix, br.get_netdata_nat64_prefix()[0])
+        self.assertDictIncludes(br.nat64_state, {
+            'PrefixManager': NAT64_STATE_IDLE,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
+
+        br.remove_route(SMALL_NAT64_PREFIX)
+        br.register_netdata()
+        self.simulator.go(10)
+
+        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
+        br.add_route(SMALL_NAT64_PREFIX, stable=False, nat64=True, prf='low')
+        br.register_netdata()
+        self.simulator.go(5)
+
+        self.assertEqual(len(br.get_netdata_nat64_prefix()), 2)
+        self.assertEqual(br.get_netdata_nat64_prefix(), [infra_nat64_prefix, SMALL_NAT64_PREFIX])
+        self.assertDictIncludes(br.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
+
+        br.remove_route(SMALL_NAT64_PREFIX)
+        br.register_netdata()
+        self.simulator.go(5)
+
+        # Case 4 Infrastructure nat64 prefix no longer presents
+        br.bash("service bind9 stop")
+        self.simulator.go(NAT64_PREFIX_REFRESH_DELAY)
+
+        local_nat64_prefix = br.get_br_nat64_prefix()
+        self.assertNotEqual(local_nat64_prefix, infra_nat64_prefix)
+        self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
+        self.assertEqual(br.get_netdata_nat64_prefix()[0], local_nat64_prefix)
+        self.assertDictIncludes(br.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_ACTIVE
+        })
+
+        # Case 5 Infrastructure nat64 prefix is recovered
+        br.bash("service bind9 start")
+        self.simulator.go(NAT64_PREFIX_REFRESH_DELAY)
+
+        self.assertEqual(br.get_br_favored_nat64_prefix(), infra_nat64_prefix)
+        self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
+        self.assertEqual(br.get_netdata_nat64_prefix()[0], infra_nat64_prefix)
+        self.assertDictIncludes(br.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
+
+        # Case 6 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")
+        self.simulator.go(NAT64_PREFIX_REFRESH_DELAY)
+
+        self.assertEqual(br.get_br_favored_nat64_prefix(), SMALL_NAT64_PREFIX)
+        self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
+        self.assertEqual(br.get_netdata_nat64_prefix()[0], SMALL_NAT64_PREFIX)
+        self.assertDictIncludes(br.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_NOT_RUNNING
+        })
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/scripts/thread-cert/border_router/test_advertising_proxy.py b/tests/scripts/thread-cert/border_router/test_advertising_proxy.py
index 0c088dc..5c66bf9 100755
--- a/tests/scripts/thread-cert/border_router/test_advertising_proxy.py
+++ b/tests/scripts/thread-cert/border_router/test_advertising_proxy.py
@@ -212,16 +212,54 @@
             self.host_check_mdns_service(host, host_address, 'my-service', service_port)
 
         #
-        # 9. Check if the expired service is removed by the Advertising Proxy.
+        # 9. Check if Advertising Proxy filters out Mesh Local and Link Local host addresses
         #
+        client.srp_client_remove_host()
+        self.simulator.go(2)
+        client.srp_client_set_host_name('my-host')
+        client.srp_client_set_host_address('2001::1', '2002::2',
+                                           client.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
+                                           client.get_ip6_address(config.ADDRESS_TYPE.LINK_LOCAL),
+                                           client.get_ip6_address(config.ADDRESS_TYPE.ML_EID))
+        client.srp_client_add_service('my-service', '_ipps._tcp', 12345)
+        client.srp_client_enable_auto_start_mode()
+        self.simulator.go(10)
+        self.check_host_and_service(server, client, [
+            '2001::1', '2002::2',
+            client.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
+            client.get_ip6_address(config.ADDRESS_TYPE.LINK_LOCAL),
+            client.get_ip6_address(config.ADDRESS_TYPE.ML_EID)
+        ], 'my-service', 12345)
+        self.host_check_mdns_service(
+            host, ['2001::1', '2002::2', client.get_ip6_address(config.ADDRESS_TYPE.OMR)[0]], 'my-service', 12345)
 
+        client.srp_client_set_host_address(client.get_ip6_address(config.ADDRESS_TYPE.LINK_LOCAL),
+                                           client.get_ip6_address(config.ADDRESS_TYPE.ML_EID), client.get_rloc())
+        self.simulator.go(10)
+        self.check_host_and_service(server, client, [
+            client.get_ip6_address(config.ADDRESS_TYPE.LINK_LOCAL),
+            client.get_ip6_address(config.ADDRESS_TYPE.ML_EID),
+            client.get_rloc()
+        ], 'my-service', 12345)
+        self.host_check_mdns_service(host, [], 'my-service', 12345)
+
+        client.srp_client_set_host_address('2005::3')
+        self.simulator.go(10)
+        self.check_host_and_service(server, client, '2005::3', 'my-service', 12345)
+        self.host_check_mdns_service(host, '2005::3', 'my-service', 12345)
+
+        #
+        # 10. Check if the expired service is removed by the Advertising Proxy.
+        #
         client.srp_client_stop()
         self.simulator.go(LEASE + 2)
 
         self.assertIsNone(host.discover_mdns_service('my-service', '_ipps._tcp', 'my-host'))
         self.assertIsNone(host.discover_mdns_service('my-service-1', '_ipps._tcp', 'my-host'))
 
-    def host_check_mdns_service(self, host, host_addr, service_instance, service_port=12345):
+    def host_check_mdns_service(self, host, host_addrs, service_instance, service_port=12345):
+        if isinstance(host_addrs, str):
+            host_addrs = [host_addrs]
         service = host.discover_mdns_service(service_instance, '_ipps._tcp', 'my-host')
         self.assertIsNotNone(service)
         self.assertEqual(service['instance'], service_instance)
@@ -230,13 +268,16 @@
         self.assertEqual(service['priority'], 0)
         self.assertEqual(service['weight'], 0)
         self.assertEqual(service['host'], 'my-host')
-        self.assertEqual(ipaddress.ip_address(service['addresses'][0]), ipaddress.ip_address(host_addr))
-        self.assertEqual(len(service['addresses']), 1)
+        self.assertEqual(len(service['addresses']), len(host_addrs))
+        self.assertEqual(sorted(map(ipaddress.ip_address, service['addresses'])),
+                         sorted(map(ipaddress.ip_address, host_addrs)))
 
-    def check_host_and_service(self, server, client, host_addr, service_instance, service_port=12345):
+    def check_host_and_service(self, server, client, host_addrs, service_instance, service_port=12345):
         """Check that we have properly registered host and service instance.
         """
 
+        if isinstance(host_addrs, str):
+            host_addrs = [host_addrs]
         client_services = client.srp_client_get_services()
         print(client_services)
         client_services = [service for service in client_services if service['instance'] == service_instance]
@@ -276,8 +317,8 @@
 
         self.assertEqual(server_host['deleted'], 'false')
         self.assertEqual(server_host['fullname'], server_service['host_fullname'])
-        self.assertEqual(len(server_host['addresses']), 1)
-        self.assertEqual(ipaddress.ip_address(server_host['addresses'][0]), ipaddress.ip_address(host_addr))
+        self.assertEqual(sorted(map(ipaddress.ip_address, server_host['addresses'])),
+                         sorted(map(ipaddress.ip_address, host_addrs)))
 
 
 class SrpClientRemoveNonExistingHost(thread_cert.TestCase):
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
index 7c2cc99..720b7bc
--- 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
@@ -93,6 +93,7 @@
         self.simulator.go(config.LEADER_STARTUP_DELAY)
         self.assertEqual('leader', br1.get_state())
         server.srp_server_set_enabled(True)
+        br1.dns_upstream_query_state = False
 
         br2.start()
         self.simulator.go(config.BORDER_ROUTER_STARTUP_DELAY)
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
index 471c83e..d31bf07
--- a/tests/scripts/thread-cert/border_router/test_dnssd_server.py
+++ b/tests/scripts/thread-cert/border_router/test_dnssd_server.py
@@ -97,6 +97,7 @@
         self.simulator.go(config.LEADER_STARTUP_DELAY)
         self.assertEqual('leader', br1.get_state())
         server.srp_server_set_enabled(True)
+        server.dns_upstream_query_state = False
 
         client1.start()
 
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 3a09b72..4617848
--- 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
@@ -114,6 +114,7 @@
         self.simulator.go(config.LEADER_STARTUP_DELAY)
         self.assertEqual('leader', br1.get_state())
         br1.srp_server_set_enabled(True)
+        br1.dns_upstream_query_state = False
 
         br2.stop_mdns_service()
         br2.stop_otbr_service()
@@ -141,6 +142,7 @@
         br2.start()
 
         self.simulator.go(config.BORDER_ROUTER_STARTUP_DELAY)
+        br2.dns_upstream_query_state = False
 
         br2_addr = br2.get_ip6_address(config.ADDRESS_TYPE.OMR)[0]
 
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 34688af..bc2bb38 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
@@ -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()
@@ -146,7 +146,21 @@
         self.assertTrue(len(router2.get_ip6_address(config.ADDRESS_TYPE.OMR)) == 1)
 
         self.assertTrue(router1.ping(router2.get_ip6_address(config.ADDRESS_TYPE.OMR)[0]))
+        self.verify_border_routing_counters(br1, {'inbound_unicast': 1, 'outbound_unicast': 1})
+        self.verify_border_routing_counters(br2, {'inbound_unicast': 1, 'outbound_unicast': 1})
         self.assertTrue(router2.ping(router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0]))
+        self.verify_border_routing_counters(br1, {'inbound_unicast': 1, 'outbound_unicast': 1})
+        self.verify_border_routing_counters(br2, {'inbound_unicast': 1, 'outbound_unicast': 1})
+        self.assertGreater(br1.get_border_routing_counters()['ra_rx'], 0)
+        self.assertGreater(br1.get_border_routing_counters()['ra_tx_success'], 0)
+        self.assertGreater(br1.get_border_routing_counters()['rs_tx_success'], 0)
+
+    def verify_border_routing_counters(self, br, expect_delta):
+        delta_counters = br.read_border_routing_counters_delta()
+        self.assertEqual(set(delta_counters.keys()), set(expect_delta.keys()))
+        for key in delta_counters:
+            self.assertEqual(delta_counters[key][0], expect_delta[key])
+            self.assertGreater(delta_counters[key][1], 0)
 
 
 if __name__ == '__main__':
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_publish_meshcop_service.py b/tests/scripts/thread-cert/border_router/test_publish_meshcop_service.py
index 25e5cd4..afc8d33 100755
--- a/tests/scripts/thread-cert/border_router/test_publish_meshcop_service.py
+++ b/tests/scripts/thread-cert/border_router/test_publish_meshcop_service.py
@@ -157,8 +157,11 @@
             self.assertEqual((state_bitmap >> 3 & 3), 2)  # Thread is attached
         self.assertEqual((state_bitmap >> 5 & 3), 1)  # high availability
         self.assertEqual((state_bitmap >> 7 & 1),
+                         br.get_state() not in ['disabled', 'detached'] and
                          br.get_backbone_router_state() != 'Disabled')  # BBR is enabled or not
-        self.assertEqual((state_bitmap >> 8 & 1), br.get_backbone_router_state() == 'Primary')  # BBR is primary or not
+        self.assertEqual((state_bitmap >> 8 & 1),
+                         br.get_state() not in ['disabled', 'detached'] and
+                         br.get_backbone_router_state() == 'Primary')  # BBR is primary or not
         self.assertEqual(service_data['txt']['nn'], br.get_network_name())
         self.assertEqual(service_data['txt']['rv'], '1')
         self.assertIn(service_data['txt']['tv'], ['1.1.0', '1.1.1', '1.2.0', '1.3.0'])
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 577b73e..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)
@@ -270,7 +266,7 @@
 
         # The routing manager may fail to send RS and will wait for 4 seconds
         # before retrying.
-        self.simulator.go(20)
+        self.simulator.go(40)
         self.collect_ipaddrs()
 
         logging.info("BR     addrs: %r", br.get_addrs())
@@ -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)
@@ -297,8 +291,8 @@
         self.assertEqual(host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA), [host_ula_address])
 
         # Router1 can ping to/from the Host on infra link.
-        self.assertTrue(router.ping(host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0]))
         self.assertTrue(host.ping(router.get_ip6_address(config.ADDRESS_TYPE.OMR)[0], backbone=True))
+        self.assertTrue(router.ping(host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0]))
 
         #
         # Case 5. Test if the linux host is still reachable if rejoin the network.
@@ -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
index 7181138..33487d8
--- a/tests/scripts/thread-cert/border_router/test_trel_connectivity.py
+++ b/tests/scripts/thread-cert/border_router/test_trel_connectivity.py
@@ -104,8 +104,14 @@
         br2 = self.nodes[BR2]
         router2 = self.nodes[ROUTER2]
 
-        if br1.get_trel_state() is None:
-            self.skipTest("TREL is not enabled")
+        if br1.is_trel_enabled() is None:
+            self.skipTest("TREL is not supported")
+
+        if br1.is_trel_enabled() == False:
+            br1.enable_trel()
+
+        if br2.is_trel_enabled() == False:
+            br2.enable_trel()
 
         br1.start()
         self.wait_node_state(br1, 'leader', 10)
diff --git a/.lgtm.yml b/tests/scripts/thread-cert/call_dbus_method.py
similarity index 73%
copy from .lgtm.yml
copy to tests/scripts/thread-cert/call_dbus_method.py
index 9051e95..913c6a5 100644
--- a/.lgtm.yml
+++ b/tests/scripts/thread-cert/call_dbus_method.py
@@ -1,5 +1,6 @@
+#!/usr/bin/env python3
 #
-#  Copyright (c) 2020, The OpenThread Authors.
+#  Copyright (c) 2022, The OpenThread Authors.
 #  All rights reserved.
 #
 #  Redistribution and use in source and binary forms, with or without
@@ -25,13 +26,21 @@
 #  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 #  POSSIBILITY OF SUCH DAMAGE.
 #
+import dbus
+import json
+import sys
 
-extraction:
-  cpp:
-    prepare:
-      packages:
-        - cmake
-        - ninja-build
-    index:
-      build_command:
-        - THREAD_VERSION=1.2 NODE_MODE=rcp OT_NATIVE_IP=1 ./script/test build
+
+def main():
+    args = sys.argv[1:]
+    bus = dbus.SystemBus()
+    interface, method_name, arguments = args[0], args[1], json.loads(args[2])
+    obj = bus.get_object('io.openthread.BorderRouter.wpan0', '/io/openthread/BorderRouter/wpan0')
+    iface = dbus.Interface(obj, interface)
+    method = getattr(iface, method_name)
+    res = method(*arguments)
+    print(json.dumps(res))
+
+
+if __name__ == '__main__':
+    main()
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
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 3878d4e..cf04cbe
--- a/tests/scripts/thread-cert/config.py
+++ b/tests/scripts/thread-cert/config.py
@@ -127,6 +127,7 @@
 
 LEADER_STARTUP_DELAY = 12
 ROUTER_STARTUP_DELAY = 10
+ED_STARTUP_DELAY = 5
 BORDER_ROUTER_STARTUP_DELAY = 20
 MAX_NEIGHBOR_AGE = 100
 INFINITE_COST_TIMEOUT = 90
@@ -153,6 +154,13 @@
 PACKET_VERIFICATION_DEFAULT = 1
 PACKET_VERIFICATION_TREL = 2
 
+# After leader reset it may retransmit link request 6 times with max 5.5s interval
+LEADER_RESET_DELAY = 41
+# After router reset it may retransmit link request 3 times with max 5.5s interval
+ROUTER_RESET_DELAY = 23
+MLE_MAX_CRITICAL_TRANSMISSION_COUNT = 6
+MLE_MAX_TRANSMISSION_COUNT = 3
+
 
 def create_default_network_data_prefix_sub_tlvs_factories():
     return {
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 5daa6ff..ab00a26
--- a/tests/scripts/thread-cert/ipv6.py
+++ b/tests/scripts/thread-cert/ipv6.py
@@ -29,10 +29,10 @@
 
 import abc
 import io
+import ipaddress
 import struct
 
 from binascii import hexlify
-from ipaddress import ip_address
 
 import common
 
@@ -93,6 +93,25 @@
         return checksum
 
 
+def synthesize_ip6_address(ip6_network: ipaddress.IPv6Network,
+                           ip4_address: ipaddress.IPv4Address) -> ipaddress.IPv6Address:
+    """ Synthesize an IPv6 address from a prefix for NAT64 and an IPv4 address.
+
+    Only supports /96 network for now.
+
+    Args:
+        ip6_network: The network for NAT64.
+        ip4_address: The IPv4 address.
+
+    Returns:
+        ipaddress.IPv6Address: The synthesized IPv6 address.
+    """
+    if ip6_network.prefixlen != 96:
+        # We are only using /96 networks in openthread
+        raise NotImplementedError("synthesize_ip6_address only supports /96 networks")
+    return ipaddress.IPv6Address(int(ip6_network.network_address) | int(ip4_address))
+
+
 class PacketFactory(object):
     """ Interface for classes that produce objects from data. """
 
@@ -209,7 +228,7 @@
         if isinstance(value, bytearray):
             value = bytes(value)
 
-        return ip_address(value)
+        return ipaddress.ip_address(value)
 
     @property
     def source_address(self):
@@ -267,7 +286,7 @@
         if isinstance(value, bytearray):
             value = bytes(value)
 
-        return ip_address(value)
+        return ipaddress.ip_address(value)
 
     @property
     def source_address(self):
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/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 3c70b7f..0c826ad
--- a/tests/scripts/thread-cert/mle.py
+++ b/tests/scripts/thread-cert/mle.py
@@ -90,6 +90,7 @@
     ACTIVE_OPERATIONAL_DATASET = 24
     PENDING_OPERATIONAL_DATASET = 25
     THREAD_DISCOVERY = 26
+    SUPERVISION_INTERVAL = 27
     CSL_CHANNEL = 80
     CSL_SYNCHRONIZED_TIMEOUT = 85
     CSL_CLOCK_ACCURACY = 86
diff --git a/tests/scripts/thread-cert/net_crypto.py b/tests/scripts/thread-cert/net_crypto.py
old mode 100755
new mode 100644
index 66533b9..d9d007f
--- a/tests/scripts/thread-cert/net_crypto.py
+++ b/tests/scripts/thread-cert/net_crypto.py
@@ -33,8 +33,6 @@
 
 from binascii import hexlify
 
-from Crypto.Cipher import AES
-
 
 class CryptoEngine:
     """ Class responsible for encryption and decryption of data. """
@@ -64,6 +62,8 @@
         """
         key, nonce, auth_data = self._crypto_material_creator.create_key_and_nonce_and_authenticated_data(message_info)
 
+        from Crypto.Cipher import AES
+
         cipher = AES.new(key, AES.MODE_CCM, nonce, mac_len=self.mic_length)
         cipher.update(auth_data)
 
@@ -83,6 +83,8 @@
         """
         key, nonce, auth_data = self._crypto_material_creator.create_key_and_nonce_and_authenticated_data(message_info)
 
+        from Crypto.Cipher import AES
+
         cipher = AES.new(key, AES.MODE_CCM, nonce, mac_len=self.mic_length)
         cipher.update(auth_data)
 
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 d8b254c..57a41b2 100755
--- a/tests/scripts/thread-cert/node.py
+++ b/tests/scripts/thread-cert/node.py
@@ -27,11 +27,13 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
+import json
 import binascii
 import ipaddress
 import logging
 import os
 import re
+import shlex
 import socket
 import subprocess
 import sys
@@ -51,6 +53,8 @@
 
 PORT_OFFSET = int(os.getenv('PORT_OFFSET', "0"))
 
+INFRA_DNS64 = int(os.getenv('NAT64', 0))
+
 
 class OtbrDocker:
     RESET_DELAY = 3
@@ -58,6 +62,7 @@
     _socat_proc = None
     _ot_rcp_proc = None
     _docker_proc = None
+    _border_routing_counters = None
 
     def __init__(self, nodeid: int, **kwargs):
         self.verbose = int(float(os.getenv('VERBOSE', 0)))
@@ -107,13 +112,17 @@
         logging.info(f'Docker image: {config.OTBR_DOCKER_IMAGE}')
         subprocess.check_call(f"docker rm -f {self._docker_name} || true", shell=True)
         CI_ENV = os.getenv('CI_ENV', '').split()
+        dns = ['--dns=127.0.0.1'] if INFRA_DNS64 == 1 else ['--dns=8.8.8.8']
+        nat64_prefix = ['--nat64-prefix', '2001:db8:1:ffff::/96'] if INFRA_DNS64 == 1 else []
         os.makedirs('/tmp/coverage/', exist_ok=True)
-        self._docker_proc = subprocess.Popen(['docker', 'run'] + CI_ENV + [
+
+        cmd = ['docker', 'run'] + CI_ENV + [
             '--rm',
             '--name',
             self._docker_name,
             '--network',
             config.BACKBONE_DOCKER_NETWORK_NAME,
+        ] + dns + [
             '-i',
             '--sysctl',
             'net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1',
@@ -128,10 +137,9 @@
             config.BACKBONE_IFNAME,
             '--trel-url',
             f'trel://{config.BACKBONE_IFNAME}',
-        ],
-                                             stdin=subprocess.DEVNULL,
-                                             stdout=sys.stdout,
-                                             stderr=sys.stderr)
+        ] + nat64_prefix
+        logging.info(' '.join(cmd))
+        self._docker_proc = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=sys.stdout, stderr=sys.stderr)
 
         launch_docker_deadline = time.time() + 300
         launch_ok = False
@@ -163,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'
@@ -358,6 +364,135 @@
 
         return dig_result
 
+    def call_dbus_method(self, *args):
+        args = shlex.join([args[0], args[1], json.dumps(args[2:])])
+        return json.loads(
+            self.bash(f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/call_dbus_method.py {args}')
+            [0])
+
+    def get_dbus_property(self, property_name):
+        return self.call_dbus_method('org.freedesktop.DBus.Properties', 'Get', 'io.openthread.BorderRouter',
+                                     property_name)
+
+    def set_dbus_property(self, property_name, property_value):
+        return self.call_dbus_method('org.freedesktop.DBus.Properties', 'Set', 'io.openthread.BorderRouter',
+                                     property_name, property_value)
+
+    def get_border_routing_counters(self):
+        counters = self.get_dbus_property('BorderRoutingCounters')
+        counters = {
+            'inbound_unicast': counters[0],
+            'inbound_multicast': counters[1],
+            'outbound_unicast': counters[2],
+            'outbound_multicast': counters[3],
+            'ra_rx': counters[4],
+            'ra_tx_success': counters[5],
+            'ra_tx_failure': counters[6],
+            'rs_rx': counters[7],
+            'rs_tx_success': counters[8],
+            'rs_tx_failure': counters[9],
+        }
+        logging.info(f'border routing counters: {counters}')
+        return counters
+
+    def _process_traffic_counters(self, counter):
+        return {
+            '4to6': {
+                'packets': counter[0],
+                'bytes': counter[1],
+            },
+            '6to4': {
+                'packets': counter[2],
+                'bytes': counter[3],
+            }
+        }
+
+    def _process_packet_counters(self, counter):
+        return {'4to6': {'packets': counter[0]}, '6to4': {'packets': counter[1]}}
+
+    def nat64_set_enabled(self, enable):
+        return self.call_dbus_method('io.openthread.BorderRouter', 'SetNat64Enabled', enable)
+
+    @property
+    def nat64_state(self):
+        state = self.get_dbus_property('Nat64State')
+        return {'PrefixManager': state[0], 'Translator': state[1]}
+
+    @property
+    def nat64_mappings(self):
+        return [{
+            'id': row[0],
+            'ip4': row[1],
+            'ip6': row[2],
+            'expiry': row[3],
+            'counters': {
+                'total': self._process_traffic_counters(row[4][0]),
+                'ICMP': self._process_traffic_counters(row[4][1]),
+                'UDP': self._process_traffic_counters(row[4][2]),
+                'TCP': self._process_traffic_counters(row[4][3]),
+            }
+        } for row in self.get_dbus_property('Nat64Mappings')]
+
+    @property
+    def nat64_counters(self):
+        res_error = self.get_dbus_property('Nat64ErrorCounters')
+        res_proto = self.get_dbus_property('Nat64ProtocolCounters')
+        return {
+            'protocol': {
+                'Total': self._process_traffic_counters(res_proto[0]),
+                'ICMP': self._process_traffic_counters(res_proto[1]),
+                'UDP': self._process_traffic_counters(res_proto[2]),
+                'TCP': self._process_traffic_counters(res_proto[3]),
+            },
+            'errors': {
+                'Unknown': self._process_packet_counters(res_error[0]),
+                'Illegal Pkt': self._process_packet_counters(res_error[1]),
+                'Unsup Proto': self._process_packet_counters(res_error[2]),
+                'No Mapping': self._process_packet_counters(res_error[3]),
+            }
+        }
+
+    @property
+    def nat64_traffic_counters(self):
+        res = self.get_dbus_property('Nat64TrafficCounters')
+        return {
+            'Total': self._process_traffic_counters(res[0]),
+            'ICMP': self._process_traffic_counters(res[1]),
+            'UDP': self._process_traffic_counters(res[2]),
+            'TCP': self._process_traffic_counters(res[3]),
+        }
+
+    @property
+    def dns_upstream_query_state(self):
+        return bool(self.get_dbus_property('DnsUpstreamQueryState'))
+
+    @dns_upstream_query_state.setter
+    def dns_upstream_query_state(self, value):
+        if type(value) is not bool:
+            raise ValueError("dns_upstream_query_state must be a bool")
+        return self.set_dbus_property('DnsUpstreamQueryState', value)
+
+    def read_border_routing_counters_delta(self):
+        old_counters = self._border_routing_counters
+        new_counters = self.get_border_routing_counters()
+        self._border_routing_counters = new_counters
+        delta_counters = {}
+        if old_counters is None:
+            delta_counters = new_counters
+        else:
+            for i in ('inbound', 'outbound'):
+                for j in ('unicast', 'multicast'):
+                    key = f'{i}_{j}'
+                    assert (key in old_counters)
+                    assert (key in new_counters)
+                    value = [new_counters[key][0] - old_counters[key][0], new_counters[key][1] - old_counters[key][1]]
+                    delta_counters[key] = value
+        delta_counters = {
+            key: value for key, value in delta_counters.items() if not isinstance(value, int) and value[0] and value[1]
+        }
+
+        return delta_counters
+
     @staticmethod
     def __unescape_dns_instance_name(name: str) -> str:
         new_name = []
@@ -987,7 +1122,7 @@
 
             addresses = lines.pop(0).strip().split('[')[1].strip(' ]').split(',')
             map(str.strip, addresses)
-            host['addresses'] = [addr for addr in addresses if addr]
+            host['addresses'] = [addr.strip() for addr in addresses if addr]
 
             host_list.append(host)
 
@@ -1018,6 +1153,8 @@
                'priority': '0',
                'weight': '0',
                'ttl': '7200',
+               'lease': '7200',
+               'key-lease': '7200',
                'TXT': ['abc=010203'],
                'host_fullname': 'my-host.default.service.arpa.',
                'host': 'my-host',
@@ -1030,6 +1167,7 @@
         cmd = 'srp server service'
         self.send_command(cmd)
         lines = self._expect_command_output()
+
         service_list = []
         while lines:
             service = {}
@@ -1044,8 +1182,8 @@
                 service_list.append(service)
                 continue
 
-            # 'subtypes', port', 'priority', 'weight', 'ttl'
-            for i in range(0, 5):
+            # 'subtypes', port', 'priority', 'weight', 'ttl', 'lease', and 'key-lease'
+            for i in range(0, 7):
                 key_value = lines.pop(0).strip().split(':')
                 service[key_value[0].strip()] = key_value[1].strip()
 
@@ -1158,11 +1296,22 @@
         self.send_command(f'srp client host address')
         self._expect_done()
 
-    def srp_client_add_service(self, instance_name, service_name, port, priority=0, weight=0, txt_entries=[]):
+    def srp_client_add_service(self,
+                               instance_name,
+                               service_name,
+                               port,
+                               priority=0,
+                               weight=0,
+                               txt_entries=[],
+                               lease=0,
+                               key_lease=0):
         txt_record = "".join(self._encode_txt_entry(entry) for entry in txt_entries)
+        if txt_record == '':
+            txt_record = '-'
         instance_name = self._escape_escapable(instance_name)
         self.send_command(
-            f'srp client service add {instance_name} {service_name} {port} {priority} {weight} {txt_record}')
+            f'srp client service add {instance_name} {service_name} {port} {priority} {weight} {txt_record} {lease} {key_lease}'
+        )
         self._expect_done()
 
     def srp_client_remove_service(self, instance_name, service_name):
@@ -1189,6 +1338,16 @@
         self.send_command(cmd)
         return int(self._expect_result('\d+'))
 
+    def srp_client_set_key_lease_interval(self, leaseinterval: int):
+        cmd = f'srp client keyleaseinterval {leaseinterval}'
+        self.send_command(cmd)
+        self._expect_done()
+
+    def srp_client_get_key_lease_interval(self) -> int:
+        cmd = 'srp client keyleaseinterval'
+        self.send_command(cmd)
+        return int(self._expect_result('\d+'))
+
     def srp_client_set_ttl(self, ttl: int):
         cmd = f'srp client ttl {ttl}'
         self.send_command(cmd)
@@ -1203,7 +1362,12 @@
     # TREL utilities
     #
 
-    def get_trel_state(self) -> Union[None, bool]:
+    def enable_trel(self):
+        cmd = 'trel enable'
+        self.send_command(cmd)
+        self._expect_done()
+
+    def is_trel_enabled(self) -> Union[None, bool]:
         states = [r'Disabled', r'Enabled']
         self.send_command('trel')
         try:
@@ -1569,6 +1733,30 @@
         self.send_command('pollperiod %d' % pollperiod)
         self._expect_done()
 
+    def get_child_supervision_interval(self):
+        self.send_command('childsupervision interval')
+        return self._expect_result(r'\d+')
+
+    def set_child_supervision_interval(self, interval):
+        self.send_command('childsupervision interval %d' % interval)
+        self._expect_done()
+
+    def get_child_supervision_check_timeout(self):
+        self.send_command('childsupervision checktimeout')
+        return self._expect_result(r'\d+')
+
+    def set_child_supervision_check_timeout(self, timeout):
+        self.send_command('childsupervision checktimeout %d' % timeout)
+        self._expect_done()
+
+    def get_child_supervision_check_failure_counter(self):
+        self.send_command('childsupervision failcounter')
+        return self._expect_result(r'\d+')
+
+    def reset_child_supervision_check_failure_counter(self):
+        self.send_command('childsupervision failcounter reset')
+        self._expect_done()
+
     def get_csl_info(self):
         self.send_command('csl')
         self._expect_done()
@@ -1776,10 +1964,10 @@
 
         #
         # Example output:
-        # | ID  | RLOC16 | Timeout    | Age        | LQ In | C_VN |R|D|N|Ver|CSL|QMsgCnt| Extended MAC     |
-        # +-----+--------+------------+------------+-------+------+-+-+-+---+---+-------+------------------+
-        # |   1 | 0xc801 |        240 |         24 |     3 |  131 |1|0|0|  3| 0 |     0 | 4ecede68435358ac |
-        # |   2 | 0xc802 |        240 |          2 |     3 |  131 |0|0|0|  3| 1 |     0 | a672a601d2ce37d8 |
+        # | ID  | RLOC16 | Timeout    | Age        | LQ In | C_VN |R|D|N|Ver|CSL|QMsgCnt|Suprvsn| Extended MAC     |
+        # +-----+--------+------------+------------+-------+------+-+-+-+---+---+-------+-------+------------------+
+        # |   1 | 0xc801 |        240 |         24 |     3 |  131 |1|0|0|  3| 0 |     0 |   129 | 4ecede68435358ac |
+        # |   2 | 0xc802 |        240 |          2 |     3 |  131 |0|0|0|  3| 1 |     0 |     0 | a672a601d2ce37d8 |
         # Done
         #
 
@@ -1810,6 +1998,7 @@
                 'ver': int(col('Ver')),
                 'csl': bool(int(col('CSL'))),
                 'qmsgcnt': int(col('QMsgCnt')),
+                'suprvsn': int(col('Suprvsn'))
             }
 
         return table
@@ -1958,7 +2147,7 @@
         self._expect_done()
 
     def get_br_omr_prefix(self):
-        cmd = 'br omrprefix'
+        cmd = 'br omrprefix local'
         self.send_command(cmd)
         return self._expect_command_output()[0]
 
@@ -1972,7 +2161,7 @@
         return omr_prefixes
 
     def get_br_on_link_prefix(self):
-        cmd = 'br onlinkprefix'
+        cmd = 'br onlinkprefix local'
         self.send_command(cmd)
         return self._expect_command_output()[0]
 
@@ -1985,10 +2174,122 @@
         return prefixes
 
     def get_br_nat64_prefix(self):
-        cmd = 'br nat64prefix'
+        cmd = 'br nat64prefix local'
         self.send_command(cmd)
         return self._expect_command_output()[0]
 
+    def get_br_favored_nat64_prefix(self):
+        cmd = 'br nat64prefix favored'
+        self.send_command(cmd)
+        return self._expect_command_output()[0].split(' ')[0]
+
+    def enable_nat64(self):
+        self.send_command(f'nat64 enable')
+        self._expect_done()
+
+    def disable_nat64(self):
+        self.send_command(f'nat64 disable')
+        self._expect_done()
+
+    def get_nat64_state(self):
+        self.send_command('nat64 state')
+        res = {}
+        for line in self._expect_command_output():
+            state = line.split(':')
+            res[state[0].strip()] = state[1].strip()
+        return res
+
+    def get_nat64_mappings(self):
+        cmd = 'nat64 mappings'
+        self.send_command(cmd)
+        result = self._expect_command_output()
+        session = None
+        session_counters = None
+        sessions = []
+
+        for line in result:
+            m = re.match(
+                r'\|\s+([a-f0-9]+)\s+\|\s+(.+)\s+\|\s+(.+)\s+\|\s+(\d+)s\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|',
+                line)
+            if m:
+                groups = m.groups()
+                if session:
+                    session['counters'] = session_counters
+                    sessions.append(session)
+                session = {
+                    'id': groups[0],
+                    'ip6': groups[1],
+                    'ip4': groups[2],
+                    'expiry': int(groups[3]),
+                }
+                session_counters = {}
+                session_counters['total'] = {
+                    '4to6': {
+                        'packets': int(groups[4]),
+                        'bytes': int(groups[5]),
+                    },
+                    '6to4': {
+                        'packets': int(groups[6]),
+                        'bytes': int(groups[7]),
+                    },
+                }
+                continue
+            if not session:
+                continue
+            m = re.match(r'\|\s+\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line)
+            if m:
+                groups = m.groups()
+                session_counters[groups[0]] = {
+                    '4to6': {
+                        'packets': int(groups[1]),
+                        'bytes': int(groups[2]),
+                    },
+                    '6to4': {
+                        'packets': int(groups[3]),
+                        'bytes': int(groups[4]),
+                    },
+                }
+        if session:
+            session['counters'] = session_counters
+            sessions.append(session)
+        return sessions
+
+    def get_nat64_counters(self):
+        cmd = 'nat64 counters'
+        self.send_command(cmd)
+        result = self._expect_command_output()
+
+        protocol_counters = {}
+        error_counters = {}
+        for line in result:
+            m = re.match(r'\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line)
+            if m:
+                groups = m.groups()
+                protocol_counters[groups[0]] = {
+                    '4to6': {
+                        'packets': int(groups[1]),
+                        'bytes': int(groups[2]),
+                    },
+                    '6to4': {
+                        'packets': int(groups[3]),
+                        'bytes': int(groups[4]),
+                    },
+                }
+                continue
+            m = re.match(r'\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line)
+            if m:
+                groups = m.groups()
+                error_counters[groups[0]] = {
+                    '4to6': {
+                        'packets': int(groups[1]),
+                    },
+                    '6to4': {
+                        'packets': int(groups[2]),
+                    },
+                }
+                continue
+        return {'protocol': protocol_counters, 'errors': error_counters}
+
     def get_netdata_nat64_prefix(self):
         prefixes = []
         routes = self.get_routes()
@@ -2011,6 +2312,8 @@
         for line in netdata:
             if line.startswith('Services:'):
                 services_section = True
+            elif line.startswith('Contexts'):
+                services_section = False
             elif services_section:
                 services.append(line.strip().split(' '))
         return services
@@ -2021,8 +2324,8 @@
 
     def get_netdata(self):
         raw_netdata = self.netdata_show()
-        netdata = {'Prefixes': [], 'Routes': [], 'Services': []}
-        key_list = ['Prefixes', 'Routes', 'Services']
+        netdata = {'Prefixes': [], 'Routes': [], 'Services': [], 'Contexts': []}
+        key_list = ['Prefixes', 'Routes', 'Services', 'Contexts']
         key = None
 
         for i in range(0, len(raw_netdata)):
@@ -2077,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()
@@ -2617,7 +2924,7 @@
         else:
             timeout = 5
 
-        self._expect(r'Received ACK in reply to notification ' r'from ([\da-f:]+)\b', timeout=timeout)
+        self._expect(r'Received ACK in reply to notification from ([\da-f:]+)\b', timeout=timeout)
         (source,) = self.pexpect.match.groups()
         source = source.decode('UTF-8')
 
@@ -2851,15 +3158,38 @@
 
         return router_table
 
-    def link_metrics_query_single_probe(self, dst_addr: str, linkmetrics_flags: str):
-        cmd = 'linkmetrics query %s single %s' % (dst_addr, linkmetrics_flags)
+    def link_metrics_query_single_probe(self, dst_addr: str, linkmetrics_flags: str, block: str = ""):
+        cmd = 'linkmetrics query %s single %s %s' % (dst_addr, linkmetrics_flags, block)
         self.send_command(cmd)
-        self._expect_done()
+        self.simulator.go(5)
+        return self._parse_linkmetrics_query_result(self._expect_command_output())
 
-    def link_metrics_query_forward_tracking_series(self, dst_addr: str, series_id: int):
-        cmd = 'linkmetrics query %s forward %d' % (dst_addr, series_id)
+    def link_metrics_query_forward_tracking_series(self, dst_addr: str, series_id: int, block: str = ""):
+        cmd = 'linkmetrics query %s forward %d %s' % (dst_addr, series_id, block)
         self.send_command(cmd)
-        self._expect_done()
+        self.simulator.go(5)
+        return self._parse_linkmetrics_query_result(self._expect_command_output())
+
+    def _parse_linkmetrics_query_result(self, lines):
+        """Parse link metrics query result"""
+
+        # Exmaple 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)',
+        #  '- Margin: 80 (dB) (Exponential Moving Average)',
+        #  '- RSSI: -20 (dBm) (Exponential Moving Average)']
+        #
+        # Or 'Link Metrics Report, status: {status}'
+
+        result = {}
+        for line in lines:
+            if line.startswith('- '):
+                k, v = line[2:].split(': ')
+                result[k] = v.split(' ')[0]
+            elif line.startswith('Link Metrics Report, status: '):
+                result['Status'] = line[29:]
+        return result
 
     def link_metrics_mgmt_req_enhanced_ack_based_probing(self,
                                                          dst_addr: str,
@@ -3381,6 +3711,8 @@
             fullname = f'{host_name}.local.'
             if fullname not in elements:
                 continue
+            if 'Add' not in elements:
+                continue
             addresses.append(elements[elements.index(fullname) + 1].split('%')[0])
 
         logging.debug(f'addresses of {host_name}: {addresses}')
@@ -3417,7 +3749,7 @@
                 assert (service['host_fullname'] == f'{host_name}.local.')
                 service['host'] = host_name
                 service['addresses'] = addresses
-        return service if 'addresses' in service and service['addresses'] else None
+        return service or None
 
     def start_radvd_service(self, prefix, slaac):
         self.bash("""cat >/etc/radvd.conf <<EOF
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/consts.py b/tests/scripts/thread-cert/pktverify/consts.py
index 9afde5a..7bf3c6a 100644
--- a/tests/scripts/thread-cert/pktverify/consts.py
+++ b/tests/scripts/thread-cert/pktverify/consts.py
@@ -164,6 +164,10 @@
 THREAD_DISCOVERY_TLV = 26
 CSL_SYNCHRONIZED_TIMEOUT = 85
 CSL_CLOCK_ACCURACY = 86
+LINK_METRICS_QUERY_TLV = 87
+LINK_METRICS_MANAGEMENT_TLV = 88
+LINK_METRICS_REPORT_TLV = 89
+LINK_PROBE_TLV = 90
 
 # Network Layer TLVs
 NL_TARGET_EID_TLV = 0
diff --git a/tests/scripts/thread-cert/pktverify/layer_fields.py b/tests/scripts/thread-cert/pktverify/layer_fields.py
index eb17e55..3b0b6b2 100644
--- a/tests/scripts/thread-cert/pktverify/layer_fields.py
+++ b/tests/scripts/thread-cert/pktverify/layer_fields.py
@@ -324,7 +324,7 @@
     'mle.tlv.link_enh_ack_flags': _auto,
     'mle.tlv.link_forward_series': _list(_auto),
     'mle.tlv.link_requested_type_id_flags': _list(_hex),
-    'mle.tlv.link_sub_tlv': _auto,
+    'mle.tlv.link_sub_tlv': _list(_auto),
     'mle.tlv.link_status_sub_tlv': _auto,
     'mle.tlv.query_id': _auto,
     'mle.tlv.metric_type_id_flags.type': _list(_hex),
diff --git a/tests/scripts/thread-cert/requirements.in b/tests/scripts/thread-cert/requirements.in
new file mode 100644
index 0000000..3cac268
--- /dev/null
+++ b/tests/scripts/thread-cert/requirements.in
@@ -0,0 +1,4 @@
+ipaddress
+pexpect
+pycryptodome
+pyshark==0.4.6
diff --git a/tests/scripts/thread-cert/requirements.txt b/tests/scripts/thread-cert/requirements.txt
index 584eae7..99b9797 100644
--- a/tests/scripts/thread-cert/requirements.txt
+++ b/tests/scripts/thread-cert/requirements.txt
@@ -1,4 +1,22 @@
-ipaddress
-pexpect
-pycryptodome
-pyshark==0.4.2.11
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+#    pip-compile requirements.in
+#
+ipaddress==1.0.23
+    # via -r requirements.in
+lxml==4.9.2
+    # via pyshark
+packaging==23.0
+    # via pyshark
+pexpect==4.8.0
+    # via -r requirements.in
+ptyprocess==0.7.0
+    # via pexpect
+py==1.11.0
+    # via pyshark
+pycryptodome==3.17
+    # via -r requirements.in
+pyshark==0.4.6
+    # via -r requirements.in
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_child_supervision.py b/tests/scripts/thread-cert/test_child_supervision.py
new file mode 100755
index 0000000..0418f80
--- /dev/null
+++ b/tests/scripts/thread-cert/test_child_supervision.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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.
+#
+
+import unittest
+
+import command
+import config
+import thread_cert
+
+# Test description:
+#
+#   This test verifies behavior child supervision.
+#
+#
+# Topology:
+#
+#  Parent (leader)
+#   |
+#   |
+#  Child (sleepy).
+
+PARENT = 1
+CHILD = 2
+
+
+class ChildSupervision(thread_cert.TestCase):
+    USE_MESSAGE_FACTORY = False
+    SUPPORT_NCP = False
+
+    TOPOLOGY = {
+        PARENT: {
+            'name': 'PARENT',
+            'mode': 'rdn',
+        },
+        CHILD: {
+            'name': 'CHILD',
+            'is_mtd': True,
+            'mode': 'n',
+        },
+    }
+
+    def test(self):
+        parent = self.nodes[PARENT]
+        child = self.nodes[CHILD]
+
+        # Form the network.
+
+        parent.start()
+        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.assertEqual(parent.get_state(), 'leader')
+
+        child.start()
+        self.simulator.go(5)
+        self.assertEqual(child.get_state(), 'child')
+        child.set_pollperiod(500)
+
+        self.assertEqual(int(child.get_child_supervision_check_failure_counter()), 0)
+
+        # Check the parent's child table.
+
+        table = parent.get_child_table()
+        self.assertEqual(len(table), 1)
+        self.assertEqual(table[1]['suprvsn'], int(child.get_child_supervision_interval()))
+
+        # Change the supervision interval on child. This should trigger an
+        # MLE Child Update exchange from child to parent so to inform parent
+        # about the change. Verify that parent is notified by checking the
+        # parent's child table.
+
+        child.set_child_supervision_interval(20)
+
+        self.simulator.go(2)
+
+        self.assertEqual(int(child.get_child_supervision_interval()), 20)
+        table = parent.get_child_table()
+        self.assertEqual(len(table), 1)
+        self.assertEqual(table[1]['suprvsn'], int(child.get_child_supervision_interval()))
+
+        # Change supervision check timeout on the child.
+
+        child.set_child_supervision_check_timeout(25)
+        self.assertEqual(int(child.get_child_supervision_check_timeout()), 25)
+
+        # Wait for multiple supervision intervals and ensure that child
+        # stays attached (child supervision working as expected).
+
+        self.simulator.go(110)
+
+        self.assertEqual(child.get_state(), 'child')
+        table = parent.get_child_table()
+        self.assertEqual(len(table), 1)
+        self.assertEqual(int(child.get_child_supervision_check_failure_counter()), 0)
+
+        # Disable supervision check on child.
+
+        child.set_child_supervision_check_timeout(0)
+
+        # Enable allowlist on parent without adding the child. After child
+        # timeout expires, the parent should remove the child from its child
+        # table.
+
+        parent.clear_allowlist()
+        parent.enable_allowlist()
+
+        table = parent.get_child_table()
+        child_timeout = table[1]['timeout']
+
+        self.simulator.go(child_timeout + 1)
+        table = parent.get_child_table()
+        self.assertEqual(len(table), 0)
+
+        # Since supervision check is disabled on the child, it should
+        # continue to stay attached to parent (since data polls are acked by
+        # radio driver).
+
+        self.assertEqual(child.get_state(), 'child')
+        self.assertEqual(int(child.get_child_supervision_check_failure_counter()), 0)
+
+        # Re-enable supervision check on child. After the check timeout the
+        # child must try to exchange "Child Update" messages with parent and
+        # then detect that parent is not responding and detach.
+
+        child.set_child_supervision_check_timeout(25)
+
+        self.simulator.go(35)
+        self.assertEqual(child.get_state(), 'detached')
+        self.assertTrue(int(child.get_child_supervision_check_failure_counter()) > 0)
+
+        # Disable allowlist on parent. Child should be able to attach again.
+
+        parent.disable_allowlist()
+        self.simulator.go(30)
+        self.assertEqual(child.get_state(), 'child')
+        child.reset_child_supervision_check_failure_counter()
+        self.assertEqual(int(child.get_child_supervision_check_failure_counter()), 0)
+
+        # Set the supervision interval to zero on child (child is asking
+        # parent not to supervise it anymore). This practically behaves
+        # the same as if parent does not support child supervision
+        # feature.
+
+        child.set_child_supervision_interval(0)
+        child.set_child_supervision_check_timeout(25)
+        self.simulator.go(2)
+
+        self.assertEqual(int(child.get_child_supervision_interval()), 0)
+        self.assertEqual(int(child.get_child_supervision_check_timeout()), 25)
+
+        table = parent.get_child_table()
+        self.assertEqual(len(table), 1)
+        self.assertEqual(table[2]['suprvsn'], int(child.get_child_supervision_interval()))
+
+        # Wait for multiple check timeouts. The child should still stay
+        # attached to parent.
+
+        self.simulator.go(100)
+        self.assertEqual(child.get_state(), 'child')
+        self.assertEqual(len(parent.get_child_table()), 1)
+        self.assertTrue(int(child.get_child_supervision_check_failure_counter()) > 0)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/scripts/thread-cert/test_detach.py b/tests/scripts/thread-cert/test_detach.py
index 2c1693d..954df98 100755
--- a/tests/scripts/thread-cert/test_detach.py
+++ b/tests/scripts/thread-cert/test_detach.py
@@ -145,10 +145,13 @@
         self.assertEqual(leader.get_state(), 'disabled')
 
         leader.start()
-        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        # leader didn't become leader after the last start(), so it re-syncs in a non-critical manner thus taking ROUTER_RESET_DELAY to recover
+        self.simulator.go(config.ROUTER_RESET_DELAY / 2)
+        self.assertEqual(leader.get_state(), 'detached')
+        self.simulator.go(config.ROUTER_RESET_DELAY / 2)
         self.assertEqual(leader.get_state(), 'leader')
         router1.start()
-        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.simulator.go(config.ROUTER_RESET_DELAY)
         self.assertEqual(router1.get_state(), 'router')
 
         leader.thread_stop()
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_history_tracker.py b/tests/scripts/thread-cert/test_history_tracker.py
index 74bbc1b..2424f2a 100755
--- a/tests/scripts/thread-cert/test_history_tracker.py
+++ b/tests/scripts/thread-cert/test_history_tracker.py
@@ -128,7 +128,7 @@
         # Start leader and child
 
         leader.start()
-        self.simulator.go(SHORT_WAIT * 2)
+        self.simulator.go(config.LEADER_RESET_DELAY)
         self.assertEqual(leader.get_state(), 'leader')
 
         child.start()
diff --git a/tests/scripts/thread-cert/test_leader_reboot_multiple_link_request.py b/tests/scripts/thread-cert/test_leader_reboot_multiple_link_request.py
new file mode 100755
index 0000000..ad9f00e
--- /dev/null
+++ b/tests/scripts/thread-cert/test_leader_reboot_multiple_link_request.py
@@ -0,0 +1,155 @@
+#!/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.
+#
+
+import copy
+import unittest
+
+import command
+import config
+import mle
+import thread_cert
+from pktverify.consts import MLE_PARENT_REQUEST, MLE_LINK_REQUEST, MLE_LINK_ACCEPT, MLE_LINK_ACCEPT_AND_REQUEST, SOURCE_ADDRESS_TLV, CHALLENGE_TLV, RESPONSE_TLV, LINK_LAYER_FRAME_COUNTER_TLV, ROUTE64_TLV, ADDRESS16_TLV, LEADER_DATA_TLV, TLV_REQUEST_TLV, VERSION_TLV
+from pktverify.packet_verifier import PacketVerifier
+from pktverify.null_field import nullField
+
+DUT_LEADER = 1
+DUT_ROUTER1 = 2
+
+# Test Purpose and Description:
+# -----------------------------
+# The purpose of this test case is to show that when the Leader is rebooted, it sends MLE_MAX_CRITICAL_TRANSMISSION_COUNT MLE link request packets if no response is received.
+#
+# Test Topology:
+# -------------
+#   Leader
+#     |
+#   Router
+#
+# DUT Types:
+# ----------
+#  Leader
+#  Router
+
+
+class Test_LeaderRebootMultipleLinkRequest(thread_cert.TestCase):
+    #USE_MESSAGE_FACTORY = False
+
+    TOPOLOGY = {
+        DUT_LEADER: {
+            'name': 'LEADER',
+            'mode': 'rdn',
+            'allowlist': [DUT_ROUTER1]
+        },
+        DUT_ROUTER1: {
+            'name': 'ROUTER',
+            'mode': 'rdn',
+            'allowlist': [DUT_LEADER]
+        },
+    }
+
+    def _setUpLeader(self):
+        self.nodes[DUT_LEADER].add_allowlist(self.nodes[DUT_ROUTER1].get_addr64())
+        self.nodes[DUT_LEADER].enable_allowlist()
+
+    def test(self):
+        self.nodes[DUT_LEADER].start()
+        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.assertEqual(self.nodes[DUT_LEADER].get_state(), 'leader')
+
+        self.nodes[DUT_ROUTER1].start()
+        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.assertEqual(self.nodes[DUT_ROUTER1].get_state(), 'router')
+
+        leader_rloc = self.nodes[DUT_LEADER].get_ip6_address(config.ADDRESS_TYPE.RLOC)
+
+        leader_rloc16 = self.nodes[DUT_LEADER].get_addr16()
+        self.nodes[DUT_LEADER].reset()
+        self.assertFalse(self.nodes[DUT_ROUTER1].ping(leader_rloc))
+        self._setUpLeader()
+
+        # Router1 will not reply to leader's link request
+        self.nodes[DUT_ROUTER1].clear_allowlist()
+
+        self.nodes[DUT_LEADER].start()
+
+        self.simulator.go(config.LEADER_RESET_DELAY)
+
+    def verify(self, pv):
+        pkts = pv.pkts
+        pv.summary.show()
+
+        LEADER = pv.vars['LEADER']
+        ROUTER = pv.vars['ROUTER']
+
+        # Verify topology is formed correctly.
+        pv.verify_attached('ROUTER', 'LEADER')
+
+        # The DUT MUST send properly formatted MLE Advertisements with
+        # an IP Hop Limit of 255 to the Link-Local All Nodes multicast
+        # address (FF02::1).
+        #  The following TLVs MUST be present in the MLE Advertisements:
+        #      - Leader Data TLV
+        #      - Route64 TLV
+        #      - Source Address TLV
+        with pkts.save_index():
+            pkts.filter_wpan_src64(LEADER).\
+                filter_mle_advertisement('Leader').\
+                must_next()
+        pkts.filter_wpan_src64(ROUTER).\
+            filter_mle_advertisement('Router').\
+            must_next()
+
+        pkts.filter_ping_request().\
+            filter_wpan_src64(ROUTER).\
+            must_next()
+
+        # The Leader MUST send MLE_MAX_CRITICAL_TRANSMISSION_COUNT multicast Link Request
+        # The following TLVs MUST be present in the Link Request:
+        #     - Challenge TLV
+        #     - Version TLV
+        #     - TLV Request TLV: Address16 TLV, Route64 TLV
+        for i in range(0, config.MLE_MAX_CRITICAL_TRANSMISSION_COUNT):
+            pkts.filter_wpan_src64(LEADER).\
+                filter_LLARMA().\
+                filter_mle_cmd(MLE_LINK_REQUEST).\
+                filter(lambda p: {
+                                CHALLENGE_TLV,
+                                VERSION_TLV,
+                                TLV_REQUEST_TLV,
+                                ADDRESS16_TLV,
+                                ROUTE64_TLV
+                                } <= set(p.mle.tlv.type) and\
+                    p.mle.tlv.addr16 is nullField and\
+                    p.mle.tlv.route64.id_mask is nullField
+                    ).\
+                must_next()
+
+
+if __name__ == '__main__':
+    unittest.main()
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/test_router_multicast_link_request.py b/tests/scripts/thread-cert/test_router_multicast_link_request.py
index 38e7681..fb36605 100755
--- a/tests/scripts/thread-cert/test_router_multicast_link_request.py
+++ b/tests/scripts/thread-cert/test_router_multicast_link_request.py
@@ -31,7 +31,7 @@
 
 import config
 import thread_cert
-from pktverify.consts import MLE_LINK_REQUEST, MLE_LINK_ACCEPT
+from pktverify.consts import MLE_LINK_REQUEST
 from pktverify.packet_verifier import PacketVerifier
 
 LEADER = 1
@@ -110,30 +110,12 @@
         self.assertEqual(self.nodes[REED].get_state(), 'router')
         self.simulator.go(LINK_ESTABLISH_DELAY_THRESHOLD + 3)
 
-    def verify(self, pv: PacketVerifier):
-        pkts = pv.pkts
-        print(pv.vars)
-        pv.summary.show()
-
-        REED = pv.vars['REED']
-        as_pkt = pkts.filter_wpan_src64(REED).filter_coap_request('/a/as', confirmable=True).must_next()
-        parent_rloc16 = as_pkt.wpan.dst16
-        as_ack_pkt = pkts.filter_wpan_src16(parent_rloc16).filter_coap_ack('/a/as').must_next()
-        become_router_timestamp = as_ack_pkt.sniff_timestamp
-
-        # REED has just received `/a/as` and become a Router
-        # REED should send Multicast Link Request after becoming Router
-        link_request_pkt = pkts.filter_wpan_src64(REED).filter_mle_cmd(MLE_LINK_REQUEST).must_next()
-        link_request_pkt.must_verify('ipv6.dst == "ff02::2"')
-
-        # REED should send Link Accept to the three Routers
-        for router in ('ROUTER1', 'ROUTER2', 'ROUTER3'):
-            with pkts.save_index():
-                pkt = pkts.filter_wpan_src64(REED).filter_wpan_dst64(
-                    pv.vars[router]).filter_mle_cmd(MLE_LINK_ACCEPT).must_next()
-                link_establish_delay = pkt.sniff_timestamp - become_router_timestamp
-                logging.info("Link to %s established in %.3f seconds", router, link_establish_delay)
-                self.assertLess(link_establish_delay, LINK_ESTABLISH_DELAY_THRESHOLD)
+        # Verify that REED has established link with all routers
+        reed_table = self.nodes[REED].router_table()
+        reed_id = self.nodes[REED].get_router_id()
+        for router in [self.nodes[ROUTER1], self.nodes[ROUTER2], self.nodes[ROUTER3]]:
+            self.assertEqual(reed_table[router.get_router_id()]['link'], 1)
+            self.assertEqual(router.router_table()[reed_id]['link'], 1)
 
 
 if __name__ == '__main__':
diff --git a/tests/scripts/thread-cert/test_router_reboot_multiple_link_request.py b/tests/scripts/thread-cert/test_router_reboot_multiple_link_request.py
new file mode 100755
index 0000000..6773b65
--- /dev/null
+++ b/tests/scripts/thread-cert/test_router_reboot_multiple_link_request.py
@@ -0,0 +1,176 @@
+#!/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.
+#
+
+import copy
+import unittest
+
+import command
+import config
+import mle
+import thread_cert
+from pktverify.consts import MLE_PARENT_REQUEST, MLE_LINK_REQUEST, MLE_LINK_ACCEPT, MLE_LINK_ACCEPT_AND_REQUEST, SOURCE_ADDRESS_TLV, CHALLENGE_TLV, RESPONSE_TLV, LINK_LAYER_FRAME_COUNTER_TLV, ROUTE64_TLV, ADDRESS16_TLV, LEADER_DATA_TLV, TLV_REQUEST_TLV, VERSION_TLV
+from pktverify.packet_verifier import PacketVerifier
+from pktverify.null_field import nullField
+
+LEADER = 1
+DUT_ROUTER = 2
+MED1 = 3
+MED2 = 4
+MED3 = 5
+MED4 = 6
+MED5 = 7
+MED6 = 8
+
+# Test Purpose and Description:
+# -----------------------------
+# The purpose of this test case is to show that when a router with > 5 children is rebooted, it sends MLE_MAX_CRITICAL_TRANSMISSION_COUNT MLE link request packets if no response is received.
+#
+# Test Topology:
+# -------------
+#   Leader
+#     |
+#   Router ------------------------+
+#   |       |     |     |    |     |
+#   MED1  MED2  MED3  MED4  MED5  MED6
+#
+# DUT Types:
+# ----------
+#  Router
+
+
+class Test_LeaderRebootMultipleLinkRequest(thread_cert.TestCase):
+    #USE_MESSAGE_FACTORY = False
+
+    TOPOLOGY = {
+        LEADER: {
+            'name': 'LEADER',
+            'mode': 'rdn',
+            'allowlist': [DUT_ROUTER]
+        },
+        DUT_ROUTER: {
+            'name': 'ROUTER',
+            'mode': 'rdn',
+            'allowlist': [LEADER, MED1, MED2, MED3, MED4, MED5, MED6]
+        },
+        MED1: {
+            'name': 'MED1',
+            'mode': 'rn',
+            'allowlist': [DUT_ROUTER]
+        },
+        MED2: {
+            'name': 'MED2',
+            'mode': 'rn',
+            'allowlist': [DUT_ROUTER]
+        },
+        MED3: {
+            'name': 'MED3',
+            'mode': 'rn',
+            'allowlist': [DUT_ROUTER]
+        },
+        MED4: {
+            'name': 'MED4',
+            'mode': 'rn',
+            'allowlist': [DUT_ROUTER]
+        },
+        MED5: {
+            'name': 'MED5',
+            'mode': 'rn',
+            'allowlist': [DUT_ROUTER]
+        },
+        MED6: {
+            'name': 'MED6',
+            'mode': 'rn',
+            'allowlist': [DUT_ROUTER]
+        },
+    }
+
+    def test(self):
+        self.nodes[LEADER].start()
+        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.assertEqual(self.nodes[LEADER].get_state(), 'leader')
+
+        self.nodes[DUT_ROUTER].start()
+        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.assertEqual(self.nodes[DUT_ROUTER].get_state(), 'router')
+
+        for medid in range(MED1, MED6 + 1):
+            self.nodes[medid].start()
+            self.simulator.go(config.ED_STARTUP_DELAY)
+            self.assertEqual(self.nodes[medid].get_state(), 'child')
+
+        self.simulator.go(config.MAX_ADVERTISEMENT_INTERVAL)
+
+        self.nodes[DUT_ROUTER].reset()
+        # Leader will not reply to router's link request
+        self.nodes[LEADER].clear_allowlist()
+
+        self.nodes[DUT_ROUTER].start()
+
+        self.simulator.go(config.LEADER_RESET_DELAY)
+
+    def verify(self, pv):
+        pkts = pv.pkts
+        pv.summary.show()
+
+        LEADER = pv.vars['LEADER']
+        ROUTER = pv.vars['ROUTER']
+
+        # Verify topology is formed correctly.
+        pv.verify_attached('ROUTER', 'LEADER')
+        for i in range(1, 7):
+            pv.verify_attached('MED%d' % i, 'ROUTER', 'MTD')
+
+        pkts.filter_wpan_src64(ROUTER).\
+            filter_mle_advertisement('Router').\
+            must_next()
+
+        # The router MUST send MLE_MAX_CRITICAL_TRANSMISSION_COUNT multicast Link Request
+        # The following TLVs MUST be present in the Link Request:
+        #     - Challenge TLV
+        #     - Version TLV
+        #     - TLV Request TLV: Address16 TLV, Route64 TLV
+        for i in range(0, config.MLE_MAX_CRITICAL_TRANSMISSION_COUNT):
+            pkts.filter_wpan_src64(ROUTER).\
+                filter_LLARMA().\
+                filter_mle_cmd(MLE_LINK_REQUEST).\
+                filter(lambda p: {
+                                CHALLENGE_TLV,
+                                VERSION_TLV,
+                                TLV_REQUEST_TLV,
+                                ADDRESS16_TLV,
+                                ROUTE64_TLV
+                                } <= set(p.mle.tlv.type) and\
+                    p.mle.tlv.addr16 is nullField and\
+                    p.mle.tlv.route64.id_mask is nullField
+                    ).\
+                must_next()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/scripts/thread-cert/test_set_mliid.py b/tests/scripts/thread-cert/test_set_mliid.py
index 760f0da..292b6ae 100755
--- a/tests/scripts/thread-cert/test_set_mliid.py
+++ b/tests/scripts/thread-cert/test_set_mliid.py
@@ -63,7 +63,7 @@
         self.nodes[LEADER].reset()
 
         self.nodes[LEADER].start()
-        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.simulator.go(config.LEADER_RESET_DELAY)
         self.assertEqual(self.nodes[LEADER].get_state(), 'leader')
 
         # Ensure ML-IID is persistent after reset.
diff --git a/tests/scripts/thread-cert/test_srp_lease.py b/tests/scripts/thread-cert/test_srp_lease.py
index 946104c..6d14711 100755
--- a/tests/scripts/thread-cert/test_srp_lease.py
+++ b/tests/scripts/thread-cert/test_srp_lease.py
@@ -137,7 +137,7 @@
         self.check_host_and_service(server, client)
 
         #
-        # 4. Clear the first service, shorten the lease time and register a second service.
+        # 4. Clear the first service, lengthen the lease time and register a second service.
         #    Verify that the lease time of the first service is not affected by new SRP
         #    registrations.
         #
@@ -156,7 +156,7 @@
         self.assertEqual(len(server.srp_server_get_hosts()), 1)
 
         #
-        # 5. Clear the second service, lengthen the lease time and register a third service.
+        # 5. Clear the second service, shorten the lease time and register a third service.
         #    Verify that the lease time of the second service is not affected by new SRP
         #    registrations.
         #
@@ -174,6 +174,19 @@
         self.assertEqual(len(server.srp_server_get_services()), 2)
         self.assertEqual(len(server.srp_server_get_hosts()), 1)
 
+        #
+        # 6. Clear the third service. The host and services should expire in lease time.
+        #    Verify that the second service and the third service are removed when their host
+        #    expires.
+        #
+        client.srp_client_clear_service('my-service3', '_ipps._tcp')
+        self.simulator.go(LEASE + 2)
+        self.assertEqual(len(server.srp_server_get_services()), 2)
+        self.assertEqual(server.srp_server_get_service('my-service2', '_ipps._tcp')['deleted'], 'true')
+        self.assertEqual(server.srp_server_get_service('my-service3', '_ipps._tcp')['deleted'], 'true')
+        self.assertEqual(len(server.srp_server_get_hosts()), 1)
+        self.assertEqual(server.srp_server_get_host('my-host')['deleted'], 'true')
+
     def check_host_and_service(self, server, client):
         """Check that we have properly registered host and service instance.
         """
diff --git a/tests/scripts/thread-cert/test_srp_many_services_mtu_check.py b/tests/scripts/thread-cert/test_srp_many_services_mtu_check.py
index 89a173f..65dfd2e 100755
--- a/tests/scripts/thread-cert/test_srp_many_services_mtu_check.py
+++ b/tests/scripts/thread-cert/test_srp_many_services_mtu_check.py
@@ -101,7 +101,7 @@
             name = chr(ord('a') + num) * 63
             client.srp_client_add_service(
                 name,
-                '_longsrvname._udp,_subtype1,_subtype2,_subtype3,_subtype4,_subtype5,_subtype6',
+                '_longlongsrvname._udp,_subtype1,_subtype2,_subtype3,_subtype4,_subtype5,_subtype6',
                 1977,
                 txt_entries=txt_info)
 
@@ -119,6 +119,23 @@
         server_services = server.srp_server_get_services()
         self.assertEqual(len(server_services), num_services)
 
+        # Remove all 8 services.
+
+        for num in range(num_services):
+            name = chr(ord('a') + num) * 63
+            client.srp_client_remove_service(name, '_longlongsrvname._udp')
+
+        self.simulator.go(10)
+
+        client_services = client.srp_client_get_services()
+        self.assertEqual(len(client_services), 0)
+
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), num_services)
+
+        for service in server_services:
+            self.assertEqual(service['deleted'], 'true')
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/scripts/thread-cert/test_srp_register_services_diff_lease.py b/tests/scripts/thread-cert/test_srp_register_services_diff_lease.py
new file mode 100755
index 0000000..6247527
--- /dev/null
+++ b/tests/scripts/thread-cert/test_srp_register_services_diff_lease.py
@@ -0,0 +1,481 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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.
+#
+
+import ipaddress
+import unittest
+
+import command
+import config
+import thread_cert
+
+# Test description:
+#
+#   This test verifies the SRP client and server behavior when services
+#   with different lease (and/or key lease) intervals are registered.
+#
+# Topology:
+#
+#     LEADER (SRP server)
+#       |
+#       |
+#     ROUTER (SRP client)
+#
+
+SERVER = 1
+CLIENT = 2
+
+
+class SrpRegisterServicesDiffLease(thread_cert.TestCase):
+    USE_MESSAGE_FACTORY = False
+    SUPPORT_NCP = False
+
+    TOPOLOGY = {
+        SERVER: {
+            'name': 'SRP_SERVER',
+            'mode': 'rdn',
+        },
+        CLIENT: {
+            'name': 'SRP_CLIENT',
+            'mode': 'rdn',
+        },
+    }
+
+    def test(self):
+        server = self.nodes[SERVER]
+        client = self.nodes[CLIENT]
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Start the server and client.
+
+        server.start()
+        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.assertEqual(server.get_state(), 'leader')
+
+        client.start()
+        self.simulator.go(config.ROUTER_STARTUP_DELAY)
+        self.assertEqual(client.get_state(), 'router')
+
+        server.srp_server_set_enabled(True)
+        client.srp_client_enable_auto_start_mode()
+
+        self.simulator.go(5)
+
+        client.srp_client_set_host_name('host')
+        client.srp_client_enable_auto_host_address()
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Add a service with specific lease and key lease and verify that
+        # it is successfully registered and seen with same lease/key-lease
+        # on server.
+
+        client.srp_client_add_service('ins1', '_test._udp', 1111, lease=60, key_lease=800)
+
+        self.simulator.go(5)
+
+        self.check_services_on_client(client, 1)
+        services = server.srp_server_get_services()
+        self.assertEqual(len(services), 1)
+        service = services[0]
+        self.assertEqual(service['fullname'], 'ins1._test._udp.default.service.arpa.')
+        self.assertEqual(service['deleted'], 'false')
+        self.assertEqual(int(service['ttl']), 60)
+        self.assertEqual(int(service['lease']), 60)
+        self.assertEqual(int(service['key-lease']), 800)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Register two more services with different lease intervals.
+
+        client.srp_client_add_service('ins2', '_test._udp', 2222, lease=30, key_lease=200)
+        client.srp_client_add_service('ins3', '_test._udp', 3333, lease=100, key_lease=1000)
+
+        self.simulator.go(10)
+
+        self.check_services_on_client(client, 3)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 3)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 60)
+                self.assertEqual(int(service['lease']), 60)
+                self.assertEqual(int(service['key-lease']), 800)
+            elif service['fullname'] == 'ins2._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 30)
+                self.assertEqual(int(service['lease']), 30)
+                self.assertEqual(int(service['key-lease']), 200)
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 100)
+                self.assertEqual(int(service['lease']), 100)
+                self.assertEqual(int(service['key-lease']), 1000)
+            else:
+                self.assertTrue(False)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Wait for longest lease time to validate that all services renew their
+        # lease successfully.
+
+        self.simulator.go(105)
+
+        self.check_services_on_client(client, 3)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 3)
+        for service in server_services:
+            self.assertEqual(service['deleted'], 'false')
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Remove two services.
+
+        client.srp_client_remove_service('ins2', '_test._udp')
+        client.srp_client_remove_service('ins3', '_test._udp')
+
+        self.simulator.go(10)
+
+        self.check_services_on_client(client, 1)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 3)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 60)
+                self.assertEqual(int(service['lease']), 60)
+                self.assertEqual(int(service['key-lease']), 800)
+            elif service['fullname'] == 'ins2._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'true')
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'true')
+            else:
+                self.assertTrue(False)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Wait for longer than key-lease of `ins2` service and check that it is
+        # removed on server.
+
+        self.simulator.go(201)
+
+        self.check_services_on_client(client, 1)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 2)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 60)
+                self.assertEqual(int(service['lease']), 60)
+                self.assertEqual(int(service['key-lease']), 800)
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'true')
+            else:
+                self.assertTrue(False)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Add both services again now with same lease intervals.
+
+        client.srp_client_add_service('ins2', '_test._udp', 2222, lease=30, key_lease=100)
+        client.srp_client_add_service('ins3', '_test._udp', 3333, lease=30, key_lease=100)
+
+        self.simulator.go(10)
+
+        self.check_services_on_client(client, 3)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 3)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 60)
+                self.assertEqual(int(service['lease']), 60)
+                self.assertEqual(int(service['key-lease']), 800)
+            elif service['fullname'] == 'ins2._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 30)
+                self.assertEqual(int(service['lease']), 30)
+                self.assertEqual(int(service['key-lease']), 100)
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 30)
+                self.assertEqual(int(service['lease']), 30)
+                self.assertEqual(int(service['key-lease']), 100)
+            else:
+                self.assertTrue(False)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Remove `ins1` while adding a new service with same key-lease as
+        # `ins1` but different lease interval.
+
+        client.srp_client_remove_service('ins1', '_test._udp')
+        client.srp_client_add_service('ins4', '_test._udp', 4444, lease=90, key_lease=800)
+
+        self.simulator.go(5)
+
+        self.check_services_on_client(client, 3)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 4)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'true')
+            elif service['fullname'] == 'ins2._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 30)
+                self.assertEqual(int(service['lease']), 30)
+                self.assertEqual(int(service['key-lease']), 100)
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 30)
+                self.assertEqual(int(service['lease']), 30)
+                self.assertEqual(int(service['key-lease']), 100)
+            elif service['fullname'] == 'ins4._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 90)
+                self.assertEqual(int(service['lease']), 90)
+                self.assertEqual(int(service['key-lease']), 800)
+            else:
+                self.assertTrue(False)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Remove two services `ins2` and `ins3` (they now have same key lease).
+
+        client.srp_client_remove_service('ins2', '_test._udp')
+        client.srp_client_remove_service('ins3', '_test._udp')
+
+        self.simulator.go(10)
+
+        self.check_services_on_client(client, 1)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 4)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'true')
+            elif service['fullname'] == 'ins2._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'true')
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'true')
+            elif service['fullname'] == 'ins4._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 90)
+                self.assertEqual(int(service['lease']), 90)
+                self.assertEqual(int(service['key-lease']), 800)
+            else:
+                self.assertTrue(False)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Add `ins1` with key-lease smaller than lease and check that
+        # client handles this properly (uses the lease value for
+        # key-lease).
+
+        client.srp_client_add_service('ins1', '_test._udp', 1111, lease=100, key_lease=90)
+
+        self.simulator.go(10)
+
+        self.check_services_on_client(client, 2)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 4)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 100)
+                self.assertEqual(int(service['lease']), 100)
+                self.assertEqual(int(service['key-lease']), 100)
+            elif service['fullname'] == 'ins2._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'true')
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'true')
+            elif service['fullname'] == 'ins4._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 90)
+                self.assertEqual(int(service['lease']), 90)
+                self.assertEqual(int(service['key-lease']), 800)
+            else:
+                self.assertTrue(False)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Change default lease and key-lease intervals on client.
+
+        client.srp_client_set_lease_interval(40)
+        self.assertEqual(client.srp_client_get_lease_interval(), 40)
+
+        client.srp_client_set_key_lease_interval(330)
+        self.assertEqual(client.srp_client_get_key_lease_interval(), 330)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Add `ins2` and `ins3`. `ins2` specifies the key-lease explicitly but
+        # leaves lease as default. `ins3` does the opposite.
+
+        client.srp_client_add_service('ins2', '_test._udp', 2222, key_lease=330)
+        client.srp_client_add_service('ins3', '_test._udp', 3333, lease=40)
+
+        self.simulator.go(10)
+
+        self.check_services_on_client(client, 4)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 4)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 100)
+                self.assertEqual(int(service['lease']), 100)
+                self.assertEqual(int(service['key-lease']), 100)
+            elif service['fullname'] == 'ins2._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 40)
+                self.assertEqual(int(service['lease']), 40)
+                self.assertEqual(int(service['key-lease']), 330)
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 40)
+                self.assertEqual(int(service['lease']), 40)
+                self.assertEqual(int(service['key-lease']), 330)
+            elif service['fullname'] == 'ins4._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 90)
+                self.assertEqual(int(service['lease']), 90)
+                self.assertEqual(int(service['key-lease']), 800)
+            else:
+                self.assertTrue(False)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Change the default lease to 50 and wait for long enough for `ins2`
+        # and `ins3` to do lease refresh. Validate that `ins2` now requests
+        # new default lease of 50 while `ins3` should stay as before.
+
+        client.srp_client_set_lease_interval(50)
+        self.assertEqual(client.srp_client_get_lease_interval(), 50)
+
+        self.simulator.go(45)
+
+        self.check_services_on_client(client, 4)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 4)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 100)
+                self.assertEqual(int(service['lease']), 100)
+                self.assertEqual(int(service['key-lease']), 100)
+            elif service['fullname'] == 'ins2._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 50)
+                self.assertEqual(int(service['lease']), 50)
+                self.assertEqual(int(service['key-lease']), 330)
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 40)
+                self.assertEqual(int(service['lease']), 40)
+                self.assertEqual(int(service['key-lease']), 330)
+            elif service['fullname'] == 'ins4._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 90)
+                self.assertEqual(int(service['lease']), 90)
+                self.assertEqual(int(service['key-lease']), 800)
+            else:
+                self.assertTrue(False)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Change the default key lease to 30. `ins3` should adopt this but
+        # since it is shorter than its explicitly specified lease the
+        # client should use same value for both lease and key-lease.
+
+        client.srp_client_set_key_lease_interval(35)
+        self.assertEqual(client.srp_client_get_key_lease_interval(), 35)
+
+        self.simulator.go(45)
+
+        self.check_services_on_client(client, 4)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 4)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 100)
+                self.assertEqual(int(service['lease']), 100)
+                self.assertEqual(int(service['key-lease']), 100)
+            elif service['fullname'] == 'ins2._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 50)
+                self.assertEqual(int(service['lease']), 50)
+                self.assertEqual(int(service['key-lease']), 330)
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 40)
+                self.assertEqual(int(service['lease']), 40)
+                self.assertEqual(int(service['key-lease']), 40)
+            elif service['fullname'] == 'ins4._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 90)
+                self.assertEqual(int(service['lease']), 90)
+                self.assertEqual(int(service['key-lease']), 800)
+            else:
+                self.assertTrue(False)
+
+        #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+        # Change the requested TTL. Wait for long enough for all
+        # services to refresh and check that the new TTL is correctly
+        # requested by the client (when it is not larger than
+        # service lease).
+
+        client.srp_client_set_ttl(65)
+        self.assertEqual(client.srp_client_get_ttl(), 65)
+
+        self.simulator.go(110)
+
+        self.check_services_on_client(client, 4)
+        server_services = server.srp_server_get_services()
+        self.assertEqual(len(server_services), 4)
+        for service in server_services:
+            if service['fullname'] == 'ins1._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 65)
+                self.assertEqual(int(service['lease']), 100)
+                self.assertEqual(int(service['key-lease']), 100)
+            elif service['fullname'] == 'ins2._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 50)
+                self.assertEqual(int(service['lease']), 50)
+                self.assertEqual(int(service['key-lease']), 330)
+            elif service['fullname'] == 'ins3._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 40)
+                self.assertEqual(int(service['lease']), 40)
+                self.assertEqual(int(service['key-lease']), 40)
+            elif service['fullname'] == 'ins4._test._udp.default.service.arpa.':
+                self.assertEqual(service['deleted'], 'false')
+                self.assertEqual(int(service['ttl']), 65)
+                self.assertEqual(int(service['lease']), 90)
+                self.assertEqual(int(service['key-lease']), 800)
+            else:
+                self.assertTrue(False)
+
+    def check_services_on_client(self, client, expected_num_services):
+        services = client.srp_client_get_services()
+        self.assertEqual(len(services), expected_num_services)
+        for service in client.srp_client_get_services():
+            self.assertIn(service['state'], ['Registered', 'ToRefresh', 'Refreshing'])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/scripts/thread-cert/thread_cert.py b/tests/scripts/thread-cert/thread_cert.py
old mode 100755
new mode 100644
index 369ed50..7277d3b
--- a/tests/scripts/thread-cert/thread_cert.py
+++ b/tests/scripts/thread-cert/thread_cert.py
@@ -38,7 +38,7 @@
 import time
 import traceback
 import unittest
-from typing import Optional, Callable, Union, Any
+from typing import Optional, Callable, Union, Mapping, Any
 
 import config
 import debug
@@ -592,3 +592,16 @@
 
         else:
             raise Exception("Route between node %d and %d is not established" % (node1, node2))
+
+    def assertDictIncludes(self, actual: Mapping[str, str], expected: Mapping[str, str]):
+        """ Asserts the `actual` dict includes the `expected` dict.
+
+        Args:
+            actual: A dict for checking.
+            expected: The expected items that the actual dict should contains.
+        """
+        for k, v in expected.items():
+            if k not in actual:
+                raise AssertionError(f"key {k} is not found in first dict")
+            if v != actual[k]:
+                raise AssertionError(f"{repr(actual[k])} != {repr(v)} for key {k}")
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_LowPower_7_1_01_SingleProbeLinkMetricsWithEnhancedAcks.py b/tests/scripts/thread-cert/v1_2_LowPower_7_1_01_SingleProbeLinkMetricsWithEnhancedAcks.py
index e14e49a..dea4198 100755
--- a/tests/scripts/thread-cert/v1_2_LowPower_7_1_01_SingleProbeLinkMetricsWithEnhancedAcks.py
+++ b/tests/scripts/thread-cert/v1_2_LowPower_7_1_01_SingleProbeLinkMetricsWithEnhancedAcks.py
@@ -202,7 +202,7 @@
         pkts.filter_wpan_src64(SED_1) \
             .filter_wpan_dst64(LEADER) \
             .filter_mle_cmd(consts.MLE_LINK_METRICS_MANAGEMENT_REQUEST) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == consts.LM_ENHANCED_ACK_CONFIGURATION_SUB_TLV) \
+            .filter(lambda p: consts.LM_ENHANCED_ACK_CONFIGURATION_SUB_TLV in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: p.mle.tlv.link_enh_ack_flags == consts.LINK_METRICS_ENH_ACK_PROBING_REGISTER) \
             .filter(lambda p: p.mle.tlv.link_requested_type_id_flags == '0a') \
             .must_next()
@@ -231,7 +231,7 @@
         # ---- Metrics Enum = 3 (RSSI)
         pkts.filter_wpan_src64(SSED_1) \
             .filter_mle_cmd(consts.MLE_LINK_METRICS_MANAGEMENT_REQUEST) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == consts.LM_ENHANCED_ACK_CONFIGURATION_SUB_TLV) \
+            .filter(lambda p: consts.LM_ENHANCED_ACK_CONFIGURATION_SUB_TLV in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: p.mle.tlv.link_enh_ack_flags == consts.LINK_METRICS_ENH_ACK_PROBING_REGISTER) \
             .filter(lambda p: p.mle.tlv.link_requested_type_id_flags == '0a0b') \
             .must_next()
@@ -295,7 +295,7 @@
         pkts.filter_wpan_src64(SSED_1) \
             .filter_wpan_dst64(LEADER) \
             .filter_mle_cmd(consts.MLE_LINK_METRICS_MANAGEMENT_REQUEST) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == consts.LM_ENHANCED_ACK_CONFIGURATION_SUB_TLV) \
+            .filter(lambda p: consts.LM_ENHANCED_ACK_CONFIGURATION_SUB_TLV in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: p.mle.tlv.link_enh_ack_flags == consts.LINK_METRICS_ENH_ACK_PROBING_CLEAR) \
             .must_next()
 
@@ -340,7 +340,7 @@
         pkts.filter_wpan_src64(SSED_1) \
             .filter_wpan_dst64(LEADER) \
             .filter_mle_cmd(consts.MLE_LINK_METRICS_MANAGEMENT_REQUEST) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == consts.LM_ENHANCED_ACK_CONFIGURATION_SUB_TLV) \
+            .filter(lambda p: consts.LM_ENHANCED_ACK_CONFIGURATION_SUB_TLV in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: p.mle.tlv.link_enh_ack_flags == consts.LINK_METRICS_ENH_ACK_PROBING_REGISTER) \
             .filter(lambda p: p.mle.tlv.link_requested_type_id_flags == '090a0b') \
             .must_next()
@@ -369,7 +369,7 @@
         pkts.filter_wpan_src64(SSED_1) \
             .filter_wpan_dst64(LEADER) \
             .filter_mle_cmd(consts.MLE_LINK_METRICS_MANAGEMENT_REQUEST) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == consts.LM_ENHANCED_ACK_CONFIGURATION_SUB_TLV) \
+            .filter(lambda p: consts.LM_ENHANCED_ACK_CONFIGURATION_SUB_TLV in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: p.mle.tlv.link_enh_ack_flags == consts.LINK_METRICS_ENH_ACK_PROBING_REGISTER) \
             .filter(lambda p: p.mle.tlv.link_requested_type_id_flags == '12') \
             .must_next()
diff --git a/tests/scripts/thread-cert/v1_2_LowPower_7_1_02_SingleProbeLinkMetricsWithoutEnhancedAck.py b/tests/scripts/thread-cert/v1_2_LowPower_7_1_02_SingleProbeLinkMetricsWithoutEnhancedAck.py
new file mode 100755
index 0000000..92ab70c
--- /dev/null
+++ b/tests/scripts/thread-cert/v1_2_LowPower_7_1_02_SingleProbeLinkMetricsWithoutEnhancedAck.py
@@ -0,0 +1,388 @@
+#!/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.
+#
+
+import unittest
+
+from config import ADDRESS_TYPE
+from mle import LinkMetricsSubTlvType
+from pktverify import consts
+from pktverify.null_field import nullField
+from pktverify.packet_verifier import PacketVerifier
+
+import config
+import thread_cert
+
+LEADER = 1
+SED_1 = 2
+SSED_1 = 3
+
+SERIES_ID = 1
+SERIES_ID_2 = 2
+POLL_PERIOD = 2000  # 2s
+
+
+class LowPower_7_1_02_SingleProbeLinkMetricsWithoutEnhancedAck(thread_cert.TestCase):
+    USE_MESSAGE_FACTORY = False
+    TOPOLOGY = {
+        LEADER: {
+            'version': '1.2',
+            'name': 'LEADER',
+            'mode': 'rdn',
+            'allowlist': [SED_1, SSED_1],
+        },
+        SED_1: {
+            'version': '1.2',
+            'name': 'SED_1',
+            'mode': '-',
+            'allowlist': [LEADER],
+        },
+        SSED_1: {
+            'version': '1.2',
+            'name': 'SSED_1',
+            'mode': '-',
+            'allowlist': [LEADER],
+        }
+    }
+    """All nodes are created with default configurations"""
+
+    def test(self):
+        self.nodes[LEADER].start()
+        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.assertEqual(self.nodes[LEADER].get_state(), 'leader')
+
+        self.nodes[SED_1].set_pollperiod(POLL_PERIOD)
+        self.nodes[SED_1].start()
+        self.simulator.go(5)
+        self.assertEqual(self.nodes[SED_1].get_state(), 'child')
+
+        self.nodes[SSED_1].set_csl_period(consts.CSL_DEFAULT_PERIOD)
+        self.nodes[SSED_1].start()
+        self.simulator.go(5)
+        self.assertEqual(self.nodes[SSED_1].get_state(), 'child')
+
+        leader_addr = self.nodes[LEADER].get_ip6_address(ADDRESS_TYPE.LINK_LOCAL)
+        sed_extaddr = self.nodes[SED_1].get_addr64()
+
+        # Step 3 - Verify connectivity by instructing each device to send an ICMPv6 Echo Request to the DUT
+        self.assertTrue(self.nodes[SED_1].ping(leader_addr, timeout=POLL_PERIOD / 1000))
+        self.assertTrue(self.nodes[SSED_1].ping(leader_addr, timeout=(2 * consts.CSL_DEFAULT_PERIOD_IN_SECOND)))
+        self.simulator.go(5)
+
+        # Step 4 - SED_1 sends a Single Probe Link Metric for RSSI using MLE Data Request
+        # MLE Data Request Payload:
+        # - TLV Request TLV (Link Metrics Report TLV specified)
+        # - Link Metrics Query TLV
+        # -- Link Metrics Query ID Sub-TLV
+        # --- Query ID = 0 (Single Probe Query)
+        # -- Link Metrics Query Options Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 3  (RSSI)
+        #
+        # In this step, SED_1 should set its TxPower to 'High'. In simulation, this will be implemented by
+        # setting Macfilter on the Rx side (Leader).
+        self.nodes[LEADER].add_allowlist(sed_extaddr, -30)
+        res = self.nodes[SED_1].link_metrics_query_single_probe(leader_addr, 'r', 'block')
+        rss_1 = int(res['RSSI'])
+        self.simulator.go(5)
+
+        # Step 6 - SED_1 sends a Single Probe Link Metric for RSSI using MLE Data Request
+        # MLE Data Request Payload:
+        # - TLV Request TLV (Link Metrics Report TLV specified)
+        # - Link Metrics Query TLV
+        # -- Link Metrics Query ID Sub-TLV
+        # --- Query ID = 0 (Single Probe Query)
+        # -- Link Metrics Query Options Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 3  (RSSI)
+        #
+        # In this step, SED_1 should set its TxPower to 'Low'.
+        self.nodes[LEADER].add_allowlist(sed_extaddr, -95)
+        res = self.nodes[SED_1].link_metrics_query_single_probe(leader_addr, 'r', 'block')
+        rss_2 = int(res['RSSI'])
+        self.simulator.go(5)
+
+        # Step 8 - Compare the rssi value in step 5 & 7, RSSI in 5 should be larger than RSSI in 7
+        self.assertTrue(rss_1 > rss_2)
+
+        # Step 9 - SSED_1 sends a Single Probe Link Metric for Layer 2 LQI using MLE Data Request
+        # MLE Data Request Payload:
+        # - TLV Request TLV (Link Metrics Report TLV specified)
+        # - Link Metrics Query TLV
+        # -- Link Metrics Query ID Sub-TLV
+        # --- Query ID = 0 (Single Probe Query)
+        # -- Link Metrics Query Options Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 1  (Layer 2 LQI)
+        self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'q', 'block')
+        self.simulator.go(5)
+
+        # Step 11 - SSED_1 sends a Single Probe Link Metric for Link Margin using MLE Data Request
+        # MLE Data Request Payload:
+        # - TLV Request TLV (Link Metrics Report TLV specified)
+        # - Link Metrics Query TLV
+        # -- Link Metrics Query ID Sub-TLV
+        # --- Query ID = 0 (Single Probe Query)
+        # -- Link Metrics Query Options Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 2  (Link Margin)
+        self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'm', 'block')
+        self.simulator.go(5)
+
+        # Step 13 - SSED_1 sends a Single Probe Link Metric using MLE Data Request
+        # MLE Data Request Payload:
+        # - TLV Request TLV (Link Metrics Report TLV specified)
+        # - Link Metrics Query TLV
+        # -- Link Metrics Query ID Sub-TLV
+        # --- Query ID = 0 (Single Probe Query)
+        # -- Link Metrics Query Options Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 1  (Layer 2 LQI)
+        # ---- Metric Enum = 2  (Link Margin)
+        # ---- Metric Enum = 3  (RSSI)
+        self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'qmr', 'block')
+        self.simulator.go(5)
+
+    def verify(self, pv):
+        pkts = pv.pkts
+        pv.summary.show()
+        LEADER = pv.vars['LEADER']
+        SED_1 = pv.vars['SED_1']
+        SSED_1 = pv.vars['SSED_1']
+
+        # Step 3 - The DUT MUST send ICMPv6 Echo Responses to both SED1 & SSED1
+        pkts.filter_wpan_src64(LEADER) \
+            .filter_wpan_dst64(SED_1) \
+            .filter_ping_reply() \
+            .must_next()
+        pkts.filter_wpan_src64(LEADER) \
+            .filter_wpan_dst64(SSED_1) \
+            .filter_ping_reply() \
+            .must_next()
+
+        # Step 4 - SED_1 sends a Single Probe Link Metric for RSSI using MLE Data Request
+        # MLE Data Request Payload:
+        # - TLV Request TLV (Link Metrics Report TLV specified)
+        # - Link Metrics Query TLV
+        # -- Link Metrics Query ID Sub-TLV
+        # --- Query ID = 0 (Single Probe Query)
+        # -- Link Metrics Query Options Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 3  (RSSI)
+        # TODO: Currently the ot-pktverify version of wireshark cannot parse Link Metrics Query Options Sub-TLV correctly. Will add check for the fields after fixing this.
+        pkts.filter_wpan_src64(SED_1) \
+           .filter_wpan_dst64(LEADER) \
+           .filter_mle_cmd(consts.MLE_DATA_REQUEST) \
+           .filter_mle_has_tlv(consts.TLV_REQUEST_TLV) \
+           .filter_mle_has_tlv(consts.LINK_METRICS_QUERY_TLV) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_QUERY_ID in p.mle.tlv.link_sub_tlv) \
+           .filter(lambda p: p.mle.tlv.query_id == 0) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_QUERY_OPTIONS in p.mle.tlv.link_sub_tlv) \
+           .must_next()
+
+        # Step 5 The DUT MUST reply to SED_1 with MLE Data Response with the following:
+        # - Link Metrics Report TLV
+        # -- Link Metrics Report Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 3  (RSSI)
+        # --- RSSI Value (1-byte)
+        pkts.filter_wpan_src64(LEADER) \
+           .filter_wpan_dst64(SED_1) \
+           .filter_mle_cmd(consts.MLE_DATA_RESPONSE) \
+           .filter_mle_has_tlv(consts.LINK_METRICS_REPORT_TLV) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_REPORT in p.mle.tlv.link_sub_tlv) \
+           .filter(lambda p: consts.LINK_METRICS_TYPE_AVERAGE_ENUM_EXPONENTIAL in p.mle.tlv.metric_type_id_flags.type) \
+           .filter(lambda p: consts.LINK_METRICS_METRIC_TYPE_ENUM_RSSI in p.mle.tlv.metric_type_id_flags.metric) \
+           .must_next()
+
+        # Step 6 - SED_1 sends a Single Probe Link Metric for RSSI using MLE Data Request
+        # MLE Data Request Payload:
+        # - TLV Request TLV (Link Metrics Report TLV specified)
+        # - Link Metrics Query TLV
+        # -- Link Metrics Query ID Sub-TLV
+        # --- Query ID = 0 (Single Probe Query)
+        # -- Link Metrics Query Options Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 3  (RSSI)
+        pkts.filter_wpan_src64(SED_1) \
+           .filter_wpan_dst64(LEADER) \
+           .filter_mle_cmd(consts.MLE_DATA_REQUEST) \
+           .filter_mle_has_tlv(consts.TLV_REQUEST_TLV) \
+           .filter_mle_has_tlv(consts.LINK_METRICS_QUERY_TLV) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_QUERY_ID in p.mle.tlv.link_sub_tlv) \
+           .filter(lambda p: p.mle.tlv.query_id == 0) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_QUERY_OPTIONS in p.mle.tlv.link_sub_tlv) \
+           .must_next()
+
+        # Step 7 The DUT MUST reply to SED_1 with MLE Data Response with the following:
+        # - Link Metrics Report TLV
+        # -- Link Metrics Report Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 3  (RSSI)
+        # --- RSSI Value (1-byte)
+        pkts.filter_wpan_src64(LEADER) \
+           .filter_wpan_dst64(SED_1) \
+           .filter_mle_cmd(consts.MLE_DATA_RESPONSE) \
+           .filter_mle_has_tlv(consts.LINK_METRICS_REPORT_TLV) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_REPORT in p.mle.tlv.link_sub_tlv) \
+           .filter(lambda p: consts.LINK_METRICS_TYPE_AVERAGE_ENUM_EXPONENTIAL in p.mle.tlv.metric_type_id_flags.type) \
+           .filter(lambda p: consts.LINK_METRICS_METRIC_TYPE_ENUM_RSSI in p.mle.tlv.metric_type_id_flags.metric) \
+           .must_next()
+
+        # Step 9 - SSED_1 sends a Single Probe Link Metric for Layer 2 LQI using MLE Data Request
+        # MLE Data Request Payload:
+        # - TLV Request TLV (Link Metrics Report TLV specified)
+        # - Link Metrics Query TLV
+        # -- Link Metrics Query ID Sub-TLV
+        # --- Query ID = 0 (Single Probe Query)
+        # -- Link Metrics Query Options Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 1  (Layer 2 LQI)
+        pkts.filter_wpan_src64(SSED_1) \
+           .filter_wpan_dst64(LEADER) \
+           .filter_mle_cmd(consts.MLE_DATA_REQUEST) \
+           .filter_mle_has_tlv(consts.TLV_REQUEST_TLV) \
+           .filter_mle_has_tlv(consts.LINK_METRICS_QUERY_TLV) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_QUERY_ID in p.mle.tlv.link_sub_tlv) \
+           .filter(lambda p: p.mle.tlv.query_id == 0) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_QUERY_OPTIONS in p.mle.tlv.link_sub_tlv) \
+           .must_next()
+
+        # Step 10 The DUT MUST reply to SSED_1 with MLE Data Response with the following:
+        # - Link Metrics Report TLV
+        # -- Link Metrics Report Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 1  (Layer 2 LQI)
+        # --- Layer 2 LQI value (1-byte)
+        pkts.filter_wpan_src64(LEADER) \
+           .filter_wpan_dst64(SSED_1) \
+           .filter_mle_cmd(consts.MLE_DATA_RESPONSE) \
+           .filter_mle_has_tlv(consts.LINK_METRICS_REPORT_TLV) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_REPORT in p.mle.tlv.link_sub_tlv) \
+           .filter(lambda p: consts.LINK_METRICS_TYPE_AVERAGE_ENUM_EXPONENTIAL in p.mle.tlv.metric_type_id_flags.type) \
+           .filter(lambda p: consts.LINK_METRICS_METRIC_TYPE_ENUM_LQI in p.mle.tlv.metric_type_id_flags.metric) \
+           .must_next()
+
+        # Step 11 - SSED_1 sends a Single Probe Link Metric for Link Margin using MLE Data Request
+        # MLE Data Request Payload:
+        # - TLV Request TLV (Link Metrics Report TLV specified)
+        # - Link Metrics Query TLV
+        # -- Link Metrics Query ID Sub-TLV
+        # --- Query ID = 0 (Single Probe Query)
+        # -- Link Metrics Query Options Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 2  (Link Margin)
+        pkts.filter_wpan_src64(SSED_1) \
+           .filter_wpan_dst64(LEADER) \
+           .filter_mle_cmd(consts.MLE_DATA_REQUEST) \
+           .filter_mle_has_tlv(consts.TLV_REQUEST_TLV) \
+           .filter_mle_has_tlv(consts.LINK_METRICS_QUERY_TLV) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_QUERY_ID in p.mle.tlv.link_sub_tlv) \
+           .filter(lambda p: p.mle.tlv.query_id == 0) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_QUERY_OPTIONS in p.mle.tlv.link_sub_tlv) \
+           .must_next()
+
+        # Step 12 The DUT MUST reply to SSED_1 with MLE Data Response with the following:
+        # - Link Metrics Report TLV
+        # -- Link Metrics Report Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 2  (Link Margin)
+        # --- Link Margin value (1-byte)
+        pkts.filter_wpan_src64(LEADER) \
+           .filter_wpan_dst64(SSED_1) \
+           .filter_mle_cmd(consts.MLE_DATA_RESPONSE) \
+           .filter_mle_has_tlv(consts.LINK_METRICS_REPORT_TLV) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_REPORT in p.mle.tlv.link_sub_tlv) \
+           .filter(lambda p: consts.LINK_METRICS_TYPE_AVERAGE_ENUM_EXPONENTIAL in p.mle.tlv.metric_type_id_flags.type) \
+           .filter(lambda p: consts.LINK_METRICS_METRIC_TYPE_ENUM_LINK_MARGIN in p.mle.tlv.metric_type_id_flags.metric) \
+           .must_next()
+
+        # Step 13 - SSED_1 sends a Single Probe Link Metric using MLE Data Request
+        # MLE Data Request Payload:
+        # - TLV Request TLV (Link Metrics Report TLV specified)
+        # - Link Metrics Query TLV
+        # -- Link Metrics Query ID Sub-TLV
+        # --- Query ID = 0 (Single Probe Query)
+        # -- Link Metrics Query Options Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 1  (Layer 2 LQI)
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 2  (Link Margin)
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 3  (RSSI)
+        pkts.filter_wpan_src64(SSED_1) \
+           .filter_wpan_dst64(LEADER) \
+           .filter_mle_cmd(consts.MLE_DATA_REQUEST) \
+           .filter_mle_has_tlv(consts.TLV_REQUEST_TLV) \
+           .filter_mle_has_tlv(consts.LINK_METRICS_QUERY_TLV) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_QUERY_ID in p.mle.tlv.link_sub_tlv) \
+           .filter(lambda p: p.mle.tlv.query_id == 0) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_QUERY_OPTIONS in p.mle.tlv.link_sub_tlv) \
+           .must_next()
+
+        # Step 14 The DUT MUST reply to SSED_1 with MLE Data Response with the following:
+        # - Link Metrics Report TLV
+        # -- Link Metrics Report Sub-TLV
+        # --- Metric Type ID Flags
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 1  (Layer 2 LQI)
+        # ---- Layer 2 LQI value (1-byte)
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 2  (Link Margin)
+        # ---- Link Margin value (1-byte)
+        # ---- Type / Average Enum = 1  (Exponential Moving Avg)
+        # ---- Metric Enum = 3  (RSSI)
+        # ---- RSSI value (1-byte)
+        pkts.filter_wpan_src64(LEADER) \
+           .filter_wpan_dst64(SSED_1) \
+           .filter_mle_cmd(consts.MLE_DATA_RESPONSE) \
+           .filter_mle_has_tlv(consts.LINK_METRICS_REPORT_TLV) \
+           .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_REPORT in p.mle.tlv.link_sub_tlv) \
+           .filter(lambda p: consts.LINK_METRICS_TYPE_AVERAGE_ENUM_EXPONENTIAL in p.mle.tlv.metric_type_id_flags.type) \
+           .filter(lambda p: consts.LINK_METRICS_METRIC_TYPE_ENUM_LQI in p.mle.tlv.metric_type_id_flags.metric) \
+           .filter(lambda p: consts.LINK_METRICS_METRIC_TYPE_ENUM_LINK_MARGIN in p.mle.tlv.metric_type_id_flags.metric) \
+           .filter(lambda p: consts.LINK_METRICS_METRIC_TYPE_ENUM_RSSI in p.mle.tlv.metric_type_id_flags.metric) \
+           .must_next()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/scripts/thread-cert/v1_2_LowPower_7_2_01_ForwardTrackingSeries.py b/tests/scripts/thread-cert/v1_2_LowPower_7_2_01_ForwardTrackingSeries.py
index b9543cf..7cb0241 100755
--- a/tests/scripts/thread-cert/v1_2_LowPower_7_2_01_ForwardTrackingSeries.py
+++ b/tests/scripts/thread-cert/v1_2_LowPower_7_2_01_ForwardTrackingSeries.py
@@ -111,9 +111,10 @@
         self.simulator.go(30)
 
         # Step 7 - SED_1 sends an MLE Data Request to retrieve aggregated Forward Series Results
-        self.nodes[SED_1].link_metrics_query_forward_tracking_series(leader_addr, SERIES_ID)
+        result = self.nodes[SED_1].link_metrics_query_forward_tracking_series(leader_addr, SERIES_ID, 'block')
+        self.assertIn("PDU Counter", result)
 
-        self.simulator.go(5)
+        #self.simulator.go(5)
 
         # Step 9 - SED_1 clears the Forward Tracking Series
         # Forward Series Flags = 0x00:
@@ -144,9 +145,8 @@
             self.simulator.go(1)
 
         # Step 19 - SSED_1 sends an MLE Data Request to retrieve aggregated Forward Series Results
-        self.nodes[SSED_1].link_metrics_query_forward_tracking_series(leader_addr, SERIES_ID_2)
-
-        self.simulator.go(5)
+        result = self.nodes[SSED_1].link_metrics_query_forward_tracking_series(leader_addr, SERIES_ID_2, 'block')
+        self.assertIn("Margin", result)
 
         # Step 21 - SSED_1 clears the Forward Series Link Metrics
         # Forward Series Flags = 0x00:
@@ -157,9 +157,9 @@
         self.simulator.go(5)
 
         # Step 23 - SSED_1 sends an MLE Data Request to retrieve aggregated Forward Series Results
-        self.nodes[SSED_1].link_metrics_query_forward_tracking_series(leader_addr, SERIES_ID_2)
-
-        self.simulator.go(5)
+        result = self.nodes[SSED_1].link_metrics_query_forward_tracking_series(leader_addr, SERIES_ID_2, 'block')
+        self.assertIn('Status', result)
+        self.assertEqual(result['Status'], 'Series ID not recognized')
 
     def verify(self, pv):
         pkts = pv.pkts
@@ -191,7 +191,7 @@
         pkts.filter_wpan_src64(SED_1) \
             .filter_wpan_dst64(LEADER) \
             .filter_mle_cmd(consts.MLE_LINK_METRICS_MANAGEMENT_REQUEST) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == LinkMetricsSubTlvType.FORWARD_PROBING_REGISTRATION) \
+            .filter(lambda p: LinkMetricsSubTlvType.FORWARD_PROBING_REGISTRATION in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: 4 in p.mle.tlv.link_forward_series)  \
             .filter(lambda p: consts.LINK_METRICS_TYPE_AVERAGE_ENUM_COUNT in p.mle.tlv.metric_type_id_flags.type) \
             .filter(lambda p: consts.LINK_METRICS_METRIC_TYPE_ENUM_PDU_COUNT in p.mle.tlv.metric_type_id_flags.metric) \
@@ -221,7 +221,7 @@
         pkts.filter_wpan_src64(SED_1) \
             .filter_wpan_dst64(LEADER) \
             .filter_mle_cmd(consts.MLE_LINK_METRICS_MANAGEMENT_REQUEST) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == LinkMetricsSubTlvType.FORWARD_PROBING_REGISTRATION) \
+            .filter(lambda p: LinkMetricsSubTlvType.FORWARD_PROBING_REGISTRATION in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: 0 in p.mle.tlv.link_forward_series) \
             .filter(lambda p: p.mle.tlv.metric_type_id_flags.type is nullField) \
             .filter(lambda p: p.mle.tlv.metric_type_id_flags.metric is nullField) \
@@ -247,7 +247,7 @@
         pkts.filter_wpan_src64(SSED_1) \
             .filter_wpan_dst64(LEADER) \
             .filter_mle_cmd(consts.MLE_LINK_METRICS_MANAGEMENT_REQUEST) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == LinkMetricsSubTlvType.FORWARD_PROBING_REGISTRATION) \
+            .filter(lambda p: LinkMetricsSubTlvType.FORWARD_PROBING_REGISTRATION in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: 2 in p.mle.tlv.link_forward_series) \
             .filter(lambda p: consts.LINK_METRICS_TYPE_AVERAGE_ENUM_EXPONENTIAL in p.mle.tlv.metric_type_id_flags.type) \
             .filter(lambda p: consts.LINK_METRICS_METRIC_TYPE_ENUM_LINK_MARGIN in p.mle.tlv.metric_type_id_flags.metric) \
@@ -284,7 +284,7 @@
             .filter_wpan_dst64(SSED_1) \
             .filter_mle_cmd(consts.MLE_DATA_RESPONSE) \
             .filter_mle_has_tlv(TlvType.LINK_METRICS_REPORT) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == LinkMetricsSubTlvType.LINK_METRICS_REPORT) \
+            .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_REPORT in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: consts.LINK_METRICS_TYPE_AVERAGE_ENUM_EXPONENTIAL in p.mle.tlv.metric_type_id_flags.type) \
             .filter(lambda p: consts.LINK_METRICS_METRIC_TYPE_ENUM_LINK_MARGIN in p.mle.tlv.metric_type_id_flags.metric) \
             .must_next()
@@ -296,7 +296,7 @@
         pkts.filter_wpan_src64(SSED_1) \
             .filter_wpan_dst64(LEADER) \
             .filter_mle_cmd(consts.MLE_LINK_METRICS_MANAGEMENT_REQUEST) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == LinkMetricsSubTlvType.FORWARD_PROBING_REGISTRATION) \
+            .filter(lambda p: LinkMetricsSubTlvType.FORWARD_PROBING_REGISTRATION in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: 0 in p.mle.tlv.link_forward_series) \
             .filter(lambda p: p.mle.tlv.metric_type_id_flags.type is nullField) \
             .filter(lambda p: p.mle.tlv.metric_type_id_flags.metric is nullField) \
@@ -330,7 +330,7 @@
             .filter_wpan_dst64(SSED_1) \
             .filter_mle_cmd(consts.MLE_DATA_RESPONSE) \
             .filter_mle_has_tlv(TlvType.LINK_METRICS_REPORT) \
-            .filter(lambda p: p.mle.tlv.link_sub_tlv == LinkMetricsSubTlvType.LINK_METRICS_STATUS) \
+            .filter(lambda p: LinkMetricsSubTlvType.LINK_METRICS_STATUS in p.mle.tlv.link_sub_tlv) \
             .filter(lambda p: p.mle.tlv.link_status_sub_tlv == consts.LINK_METRICS_STATUS_SERIES_ID_NOT_RECOGNIZED) \
             .must_next()
 
diff --git a/tests/scripts/thread-cert/v1_2_router_5_1_1.py b/tests/scripts/thread-cert/v1_2_router_5_1_1.py
index 20173f5..6f8b01c 100755
--- a/tests/scripts/thread-cert/v1_2_router_5_1_1.py
+++ b/tests/scripts/thread-cert/v1_2_router_5_1_1.py
@@ -136,31 +136,7 @@
         status_tlv = msg.get_coap_message_tlv(network_layer.Status)
         self.assertEqual(network_layer.StatusValues.SUCCESS, status_tlv.status)
 
-        # 8 - Router_1 sends a multicast Link Request Message
-        msg = router_messages.next_mle_message(mle.CommandType.LINK_REQUEST)
-        msg.assertMleMessageContainsTlv(mle.SourceAddress)
-        msg.assertMleMessageContainsTlv(mle.LeaderData)
-        msg.assertMleMessageContainsTlv(mle.Challenge)
-        msg.assertMleMessageContainsTlv(mle.Version)
-        msg.assertMleMessageContainsTlv(mle.TlvRequest)
-        assert msg.get_mle_message_tlv(mle.Version).version >= 3
-
-        tlv_request = msg.get_mle_message_tlv(mle.TlvRequest)
-        self.assertIn(mle.TlvType.LINK_MARGIN, tlv_request.tlvs)
-
-        # 9 - Leader sends a Unicast Link Accept
-        msg = leader_messages.next_mle_message(mle.CommandType.LINK_ACCEPT_AND_REQUEST)
-        msg.assertMleMessageContainsTlv(mle.SourceAddress)
-        msg.assertMleMessageContainsTlv(mle.LeaderData)
-        msg.assertMleMessageContainsTlv(mle.Response)
-        msg.assertMleMessageContainsTlv(mle.LinkLayerFrameCounter)
-        msg.assertMleMessageContainsTlv(mle.Version)
-        msg.assertMleMessageContainsTlv(mle.LinkMargin)
-        msg.assertMleMessageContainsOptionalTlv(mle.MleFrameCounter)
-        msg.assertMleMessageContainsOptionalTlv(mle.Challenge)
-        assert msg.get_mle_message_tlv(mle.Version).version >= 3
-
-        # 10 - Router_1 Transmit MLE advertisements
+        # 8 - Router_1 Transmit MLE advertisements
         msg = router_messages.next_mle_message(mle.CommandType.ADVERTISEMENT)
         msg.assertSentWithHopLimit(255)
         msg.assertSentToDestinationAddress('ff02::1')
@@ -168,7 +144,7 @@
         msg.assertMleMessageContainsTlv(mle.LeaderData)
         msg.assertMleMessageContainsTlv(mle.Route64)
 
-        # 11 - Verify connectivity by sending an ICMPv6 Echo Request to the DUT link local address
+        # 9 - Verify connectivity by sending an ICMPv6 Echo Request to the DUT link local address
         self.assertTrue(self.nodes[LEADER].ping(self.nodes[ROUTER_1].get_linklocal()))
         self.assertTrue(self.nodes[ROUTER_1].ping(self.nodes[LEADER].get_linklocal()))
 
diff --git a/tests/scripts/thread-cert/v1_2_test_backbone_router_service.py b/tests/scripts/thread-cert/v1_2_test_backbone_router_service.py
index 1a9d54d..36d5264 100755
--- a/tests/scripts/thread-cert/v1_2_test_backbone_router_service.py
+++ b/tests/scripts/thread-cert/v1_2_test_backbone_router_service.py
@@ -144,7 +144,7 @@
             self.nodes[BBR_1].set_domain_prefix(config.DOMAIN_PREFIX)
             self.nodes[BBR_1].enable_backbone_router()
             self.nodes[BBR_1].start()
-            WAIT_TIME = WAIT_ATTACH + ROUTER_SELECTION_JITTER
+            WAIT_TIME = config.ROUTER_RESET_DELAY
             self.simulator.go(WAIT_TIME)
             self.assertEqual(self.nodes[BBR_1].get_state(), 'router')
             WAIT_TIME = BBR_REGISTRATION_JITTER + WAIT_REDUNDANCE
@@ -229,7 +229,7 @@
         self.nodes[BBR_2].enable_backbone_router()
         self.nodes[BBR_2].interface_up()
         self.nodes[BBR_2].thread_start()
-        WAIT_TIME = WAIT_ATTACH + ROUTER_SELECTION_JITTER
+        WAIT_TIME = config.ROUTER_RESET_DELAY
         self.simulator.go(WAIT_TIME)
         self.assertEqual(self.nodes[BBR_2].get_state(), 'router')
         WAIT_TIME = BBR_REGISTRATION_JITTER + WAIT_REDUNDANCE
diff --git a/tests/scripts/thread-cert/v1_2_test_csl_transmission.py b/tests/scripts/thread-cert/v1_2_test_csl_transmission.py
index fa358f4..a799922 100755
--- a/tests/scripts/thread-cert/v1_2_test_csl_transmission.py
+++ b/tests/scripts/thread-cert/v1_2_test_csl_transmission.py
@@ -72,7 +72,6 @@
 
         ssed_messages = self.simulator.get_messages_sent_by(SSED_1)
         msg = ssed_messages.next_mle_message(mle.CommandType.CHILD_UPDATE_REQUEST)
-        msg.assertMleMessageDoesNotContainTlv(mle.CslChannel)
 
         self.nodes[SSED_1].set_csl_channel(consts.CSL_DEFAULT_CHANNEL)
         self.simulator.go(1)
@@ -89,7 +88,6 @@
 
         ssed_messages = self.simulator.get_messages_sent_by(SSED_1)
         msg = ssed_messages.next_mle_message(mle.CommandType.CHILD_UPDATE_REQUEST)
-        msg.assertMleMessageDoesNotContainTlv(mle.CslChannel)
 
         self.nodes[SSED_1].set_csl_period(0)
         self.assertFalse(self.nodes[LEADER].ping(self.nodes[SSED_1].get_rloc()))
@@ -127,7 +125,7 @@
 
         # Check if SSED is able to resynchronize with the parent after it is gone longer than the timeout
         self.nodes[LEADER].start()
-        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.simulator.go(config.LEADER_RESET_DELAY)
         self.nodes[SSED_1].set_csl_timeout(8)
         self.nodes[SSED_1].set_timeout(10)
         self.simulator.go(2)
@@ -135,7 +133,7 @@
         self.simulator.go(25)
         self.flush_all()
         self.nodes[LEADER].start()
-        self.simulator.go(config.LEADER_STARTUP_DELAY)
+        self.simulator.go(config.LEADER_RESET_DELAY)
         self.assertEqual(self.nodes[LEADER].get_state(), 'leader')
         self.simulator.go(5)
         self.assertEqual(self.nodes[SSED_1].get_state(), 'child')
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 a424e99..c6d369e 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
@@ -838,6 +838,7 @@
         # Turn off Router 1.1 and turn on Router 1.2
         self.nodes[ROUTER_1_1].stop()
         self.nodes[ROUTER_1_2].start()
+        self.simulator.go(config.ROUTER_RESET_DELAY)
         for id in [FED_1, MED_1, SED_1]:
             self.simulator.go(config.DEFAULT_CHILD_TIMEOUT + WAIT_REDUNDANCE)
 
diff --git a/tests/scripts/thread-cert/v1_2_test_single_probe.py b/tests/scripts/thread-cert/v1_2_test_single_probe.py
index b929479..93a8f67 100755
--- a/tests/scripts/thread-cert/v1_2_test_single_probe.py
+++ b/tests/scripts/thread-cert/v1_2_test_single_probe.py
@@ -70,40 +70,48 @@
         leader_messages = self.simulator.get_messages_sent_by(LEADER)
 
         # SSED_1 sends a Single Probe Link Metrics for L2 PDU count using MLE Data Request
-        self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'p')
-        self.simulator.go(5)
+        result = self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'p', 'block')
+        self.assertIn('PDU Counter', result)
+        self.assertEqual(len(result), 1)
 
         leader_messages = self.simulator.get_messages_sent_by(LEADER)
         msg = leader_messages.next_mle_message(mle.CommandType.DATA_RESPONSE)
         msg.assertMleMessageContainsTlv(mle.LinkMetricsReport)
 
         # SSED_1 sends a Single Probe Link Metrics for L2 LQI using MLE Data Request
-        self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'q')
-        self.simulator.go(5)
+        result = self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'q', 'block')
+        self.assertIn('LQI', result)
+        self.assertEqual(len(result), 1)
 
         leader_messages = self.simulator.get_messages_sent_by(LEADER)
         msg = leader_messages.next_mle_message(mle.CommandType.DATA_RESPONSE)
         msg.assertMleMessageContainsTlv(mle.LinkMetricsReport)
 
         # SSED_1 sends a Single Probe Link Metrics for Link Margin using MLE Data Request
-        self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'm')
-        self.simulator.go(5)
+        result = self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'm', 'block')
+        self.assertIn('Margin', result)
+        self.assertEqual(len(result), 1)
 
         leader_messages = self.simulator.get_messages_sent_by(LEADER)
         msg = leader_messages.next_mle_message(mle.CommandType.DATA_RESPONSE)
         msg.assertMleMessageContainsTlv(mle.LinkMetricsReport)
 
         # SSED_1 sends a Single Probe Link Metrics for Link Margin using MLE Data Request
-        self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'r')
-        self.simulator.go(5)
+        result = self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'r', 'block')
+        self.assertIn('RSSI', result)
+        self.assertEqual(len(result), 1)
 
         leader_messages = self.simulator.get_messages_sent_by(LEADER)
         msg = leader_messages.next_mle_message(mle.CommandType.DATA_RESPONSE)
         msg.assertMleMessageContainsTlv(mle.LinkMetricsReport)
 
         # SSED_1 sends a Single Probe Link Metrics for all metrics using MLE Data Request
-        self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'pqmr')
-        self.simulator.go(5)
+        result = self.nodes[SSED_1].link_metrics_query_single_probe(leader_addr, 'pqmr', 'block')
+        self.assertIn('PDU Counter', result)
+        self.assertIn('LQI', result)
+        self.assertIn('Margin', result)
+        self.assertIn('RSSI', result)
+        self.assertEqual(len(result), 4)
 
         leader_messages = self.simulator.get_messages_sent_by(LEADER)
         msg = leader_messages.next_mle_message(mle.CommandType.DATA_RESPONSE)
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/build.sh b/tests/toranj/build.sh
index 4c25b1a..0942813 100755
--- a/tests/toranj/build.sh
+++ b/tests/toranj/build.sh
@@ -68,10 +68,13 @@
 coverage=no
 tests=no
 
+ot_coverage=OFF
+
 while [ $# -ge 2 ]; do
     case $1 in
         -c | --enable-coverage)
             coverage=yes
+            ot_coverage=ON
             shift
             ;;
         -t | --enable-tests)
@@ -104,14 +107,6 @@
     "--enable-ncp"
 )
 
-cli_configure_options=(
-    "--disable-docs"
-    "--enable-tests=$tests"
-    "--enable-coverage=$coverage"
-    "--enable-ftd"
-    "--enable-cli"
-)
-
 posix_configure_options=(
     "--disable-docs"
     "--enable-tests=$tests"
@@ -198,30 +193,25 @@
         echo "==================================================================================================="
         echo "Building OpenThread CLI FTD mode with simulation platform (radios determined by config)"
         echo "==================================================================================================="
-        ./bootstrap || die "bootstrap failed"
         cd "${top_builddir}" || die "cd failed"
-        cppflags_config='-DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"../tests/toranj/openthread-core-toranj-config-simulation.h\"'
-        ${top_srcdir}/configure \
-            CPPFLAGS="$cppflags_config" \
-            --with-examples=simulation \
-            "${cli_configure_options[@]}" || die
-        make -j 8 || die
+        cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
+            -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
+            -DOT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
+            "${top_srcdir}" || die
+        ninja || die
         ;;
 
     cli-15.4)
         echo "==================================================================================================="
         echo "Building OpenThread CLI FTD mode with simulation platform - 15.4 radio"
         echo "==================================================================================================="
-        cppflags_config='-DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"../tests/toranj/openthread-core-toranj-config-simulation.h\"'
-        cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE=1"
-        cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE=0"
-        ./bootstrap || die "bootstrap failed"
         cd "${top_builddir}" || die "cd failed"
-        ${top_srcdir}/configure \
-            CPPFLAGS="$cppflags_config" \
-            --with-examples=simulation \
-            "${cli_configure_options[@]}" || die
-        make -j 8 || die
+        cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
+            -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
+            -DOT_15_4=ON -DOT_TREL=OFF \
+            -DOT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
+            "${top_srcdir}" || die
+        ninja || die
         cp -p ${top_builddir}/examples/apps/cli/ot-cli-ftd ${top_builddir}/examples/apps/cli/ot-cli-ftd-15.4
         ;;
 
@@ -229,16 +219,13 @@
         echo "==================================================================================================="
         echo "Building OpenThread CLI FTD mode with simulation platform - TREL radio"
         echo "==================================================================================================="
-        cppflags_config='-DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"../tests/toranj/openthread-core-toranj-config-simulation.h\"'
-        cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE=0"
-        cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE=1"
-        ./bootstrap || die "bootstrap failed"
         cd "${top_builddir}" || die "cd failed"
-        ${top_srcdir}/configure \
-            CPPFLAGS="$cppflags_config" \
-            --with-examples=simulation \
-            "${cli_configure_options[@]}" || die
-        make -j 8 || die
+        cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
+            -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
+            -DOT_15_4=OFF -DOT_TREL=ON \
+            -DOT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
+            "${top_srcdir}" || die
+        ninja || die
         cp -p ${top_builddir}/examples/apps/cli/ot-cli-ftd ${top_builddir}/examples/apps/cli/ot-cli-ftd-trel
         ;;
 
@@ -246,16 +233,13 @@
         echo "==================================================================================================="
         echo "Building OpenThread NCP FTD mode with simulation platform - multi radio (15.4 + TREL)"
         echo "==================================================================================================="
-        cppflags_config='-DOPENTHREAD_PROJECT_CORE_CONFIG_FILE=\"../tests/toranj/openthread-core-toranj-config-simulation.h\"'
-        cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE=1"
-        cppflags_config="${cppflags_config} -DOPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE=1"
-        ./bootstrap || die "bootstrap failed"
         cd "${top_builddir}" || die "cd failed"
-        ${top_srcdir}/configure \
-            CPPFLAGS="$cppflags_config" \
-            --with-examples=simulation \
-            "${cli_configure_options[@]}" || die
-        make -j 8 || die
+        cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
+            -DOT_APP_CLI=ON -DOT_APP_NCP=OFF -DOT_APP_RCP=OFF \
+            -DOT_15_4=ON -DOT_TREL=ON \
+            -DOT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
+            "${top_srcdir}" || die
+        ninja || die
         cp -p ${top_builddir}/examples/apps/cli/ot-cli-ftd ${top_builddir}/examples/apps/cli/ot-cli-ftd-15.4-trel
         ;;
 
@@ -344,7 +328,8 @@
         echo "Building OpenThread (NCP/CLI for FTD/MTD/RCP mode) with simulation platform using cmake"
         echo "===================================================================================================="
         cd "${top_builddir}" || die "cd failed"
-        cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=on -DOT_APP_CLI=on \
+        cmake -GNinja -DOT_PLATFORM=simulation -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_COVERAGE=${ot_coverage} \
+            -DOT_APP_CLI=ON -DOT_APP_NCP=ON -DOT_APP_RCP=ON \
             -DOT_CONFIG=../tests/toranj/openthread-core-toranj-config-simulation.h \
             "${top_srcdir}" || die
         ninja || die
@@ -355,7 +340,7 @@
         echo "Building OpenThread POSIX using cmake"
         echo "===================================================================================================="
         cd "${top_builddir}" || die "cd failed"
-        cmake -GNinja -DOT_PLATFORM=posix -DOT_COMPILE_WARNING_AS_ERROR=on -DOT_APP_CLI=off \
+        cmake -GNinja -DOT_PLATFORM=posix -DOT_COMPILE_WARNING_AS_ERROR=ON -DOT_APP_CLI=OFF \
             -DOT_CONFIG=../tests/toranj/openthread-core-toranj-config-posix.h \
             "${top_srcdir}" || die
         ninja || die
diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py
index 3bfb7a8..fd24205 100644
--- a/tests/toranj/cli/cli.py
+++ b/tests/toranj/cli/cli.py
@@ -46,6 +46,12 @@
 JOIN_TYPE_ROUTER = 'router'
 JOIN_TYPE_END_DEVICE = 'ed'
 JOIN_TYPE_SLEEPY_END_DEVICE = 'sed'
+JOIN_TYPE_REED = 'reed'
+
+# for use as `radios` parameter in `Node.__init__()`
+RADIO_15_4 = "-15.4"
+RADIO_TREL = "-trel"
+RADIO_15_4_TREL = "-15.4-trel"
 
 # ----------------------------------------------------------------------------------------------------------------------
 
@@ -106,26 +112,28 @@
 
     _all_nodes = weakref.WeakSet()
 
-    def __init__(self, verbose=_VERBOSE):
+    def __init__(self, radios='', index=None, verbose=_VERBOSE):
         """Creates a new `Node` instance"""
 
-        index = Node._cur_index
-        Node._cur_index += 1
+        if index is None:
+            index = Node._cur_index
+            Node._cur_index += 1
 
         self._index = index
         self._verbose = verbose
 
-        if Node._SAVE_LOGS:
-            self._log_file = open(self._LOG_FNAME + str(index) + '.log', 'wb')
-        else:
-            self._log_file = None
+        cmd = f'{self._OT_CLI_FTD}{radios} --time-speed={self._SPEED_UP_FACTOR} '
 
-        cmd = f'{self._OT_CLI_FTD} --time-speed={self._SPEED_UP_FACTOR} {self._index}'
+        if Node._SAVE_LOGS:
+            log_file_name = self._LOG_FNAME + str(index) + '.log'
+            cmd = cmd + f'--log-file={log_file_name} '
+
+        cmd = cmd + f'{self._index}'
 
         if self._verbose:
             _log(f'$ Node{index}.__init__() cmd: `{cmd}`')
 
-        self._cli_process = pexpect.popen_spawn.PopenSpawn(cmd, logfile=self._log_file)
+        self._cli_process = pexpect.popen_spawn.PopenSpawn(cmd)
         Node._all_nodes.add(self)
 
     def __del__(self):
@@ -187,8 +195,8 @@
         outputs = self.cli(cmd, *args)
         verify(len(outputs) == 0)
 
-    def _cli_single_output(self, cmd, expected_outputs=None):
-        outputs = self.cli(cmd)
+    def _cli_single_output(self, cmd, *args, expected_outputs=None):
+        outputs = self.cli(cmd, *args)
         verify(len(outputs) == 1)
         verify((expected_outputs is None) or (outputs[0] in expected_outputs))
         return outputs[0]
@@ -204,7 +212,10 @@
     # cli commands
 
     def get_state(self):
-        return self._cli_single_output('state', ['detached', 'child', 'router', 'leader', 'disabled'])
+        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')
@@ -260,6 +271,18 @@
     def set_router_selection_jitter(self, jitter):
         self._cli_no_output('routerselectionjitter', jitter)
 
+    def get_router_eligible(self):
+        return self._cli_single_output('routereligible')
+
+    def set_router_eligible(self, enable):
+        self._cli_no_output('routereligible', enable)
+
+    def get_context_reuse_delay(self):
+        return self._cli_single_output('contextreusedelay')
+
+    def set_context_reuse_delay(self, delay):
+        self._cli_no_output('contextreusedelay', delay)
+
     def interface_up(self):
         self._cli_no_output('ifconfig up')
 
@@ -275,9 +298,18 @@
     def thread_stop(self):
         self._cli_no_output('thread stop')
 
+    def get_rloc16(self):
+        return self._cli_single_output('rloc16')
+
     def get_ip_addrs(self):
         return self.cli('ipaddr')
 
+    def add_ip_addr(self, address):
+        self._cli_no_output('ipaddr add', address)
+
+    def remove_ip_addr(self, address):
+        self._cli_no_output('ipaddr del', address)
+
     def get_mleid_ip_addr(self):
         return self._cli_single_output('ipaddr mleid')
 
@@ -287,6 +319,159 @@
     def get_rloc_ip_addr(self):
         return self._cli_single_output('ipaddr rloc')
 
+    def get_mesh_local_prefix(self):
+        return self._cli_single_output('prefix meshlocal')
+
+    def get_ip_maddrs(self):
+        return self.cli('ipmaddr')
+
+    def add_ip_maddr(self, maddr):
+        return self._cli_no_output('ipmaddr add', maddr)
+
+    def get_pollperiod(self):
+        return self._cli_single_output('pollperiod')
+
+    def set_pollperiod(self, period):
+        self._cli_no_output('pollperiod', period)
+
+    def get_partition_id(self):
+        return self._cli_single_output('partitionid')
+
+    def get_nexthop(self, rloc16):
+        return self._cli_single_output('nexthop', rloc16)
+
+    def get_parent_info(self):
+        outputs = self.cli('parent')
+        result = {}
+        for line in outputs:
+            fields = line.split(':')
+            result[fields[0].strip()] = fields[1].strip()
+        return result
+
+    def get_child_table(self):
+        return Node.parse_table(self.cli('child table'))
+
+    def get_neighbor_table(self):
+        return Node.parse_table(self.cli('neighbor table'))
+
+    def get_router_table(self):
+        return Node.parse_table(self.cli('router table'))
+
+    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
+
+    def get_netdata(self):
+        outputs = self.cli('netdata show')
+        outputs = [line.strip() for line in outputs]
+        routes_index = outputs.index('Routes:')
+        services_index = outputs.index('Services:')
+        contexts_index = outputs.index('Contexts:')
+        result = {}
+        result['prefixes'] = outputs[1:routes_index]
+        result['routes'] = outputs[routes_index + 1:services_index]
+        result['services'] = outputs[services_index + 1:contexts_index]
+        result['contexts'] = outputs[contexts_index + 1:]
+
+        return result
+
+    def get_netdata_prefixes(self):
+        return self.get_netdata()['prefixes']
+
+    def get_netdata_routes(self):
+        return self.get_netdata()['routes']
+
+    def get_netdata_services(self):
+        return self.get_netdata()['services']
+
+    def get_netdata_contexts(self):
+        return self.get_netdata()['contexts']
+
+    def get_netdata_versions(self):
+        leaderdata = Node.parse_list(self.cli('leaderdata'))
+        return (int(leaderdata['Data Version']), int(leaderdata['Stable Data Version']))
+
+    def add_prefix(self, prefix, flags=None, prf=None):
+        return self._cli_no_output('prefix add', prefix, flags, prf)
+
+    def add_route(self, prefix, flags=None, prf=None):
+        return self._cli_no_output('route add', prefix, flags, prf)
+
+    def remove_prefix(self, prefix):
+        return self._cli_no_output('prefix remove', prefix)
+
+    def register_netdata(self):
+        self._cli_no_output('netdata register')
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # ping and counters
+
+    def ping(self, address, size=0, count=1, verify_success=True):
+        outputs = self.cli('ping', address, size, count)
+        m = re.match(r'(\d+) packets transmitted, (\d+) packets received.', outputs[-1].strip())
+        verify(m is not None)
+        verify(int(m.group(1)) == count)
+        if verify_success:
+            verify(int(m.group(2)) == count)
+
+    def get_mle_counter(self):
+        return self.cli('counters mle')
+
+    def get_br_counter_unicast_outbound_packets(self):
+        outputs = self.cli('counters br')
+        for line in outputs:
+            m = re.match(r'Outbound Unicast: Packets (\d+) Bytes (\d+)', line.strip())
+            if m is not None:
+                counter = int(m.group(1))
+                break
+        else:
+            verify(False)
+        return counter
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # UDP
+
+    def udp_open(self):
+        self._cli_no_output('udp open')
+
+    def udp_close(self):
+        self._cli_no_output('udp close')
+
+    def udp_bind(self, address, port):
+        self._cli_no_output('udp bind', address, port)
+
+    def udp_send(self, address, port, text):
+        self._cli_no_output('udp send', address, port, '-t', text)
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # multiradio
+
+    def multiradio_get_radios(self):
+        return self._cli_single_output('multiradio')
+
+    def multiradio_get_neighbor_list(self):
+        return self.cli('multiradio neighbor list')
+
     #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     # SRP client
 
@@ -297,10 +482,10 @@
         self._cli_no_output('srp client stop')
 
     def srp_client_get_state(self):
-        return self._cli_single_output('srp client state', ['Enabled', 'Disabled'])
+        return self._cli_single_output('srp client state', expected_outputs=['Enabled', 'Disabled'])
 
     def srp_client_get_auto_start_mode(self):
-        return self._cli_single_output('srp client autostart', ['Enabled', 'Disabled'])
+        return self._cli_single_output('srp client autostart', expected_outputs=['Enabled', 'Disabled'])
 
     def srp_client_enable_auto_start_mode(self):
         self._cli_no_output('srp client autostart enable')
@@ -338,9 +523,18 @@
     def srp_client_get_host_address(self):
         return self.cli('srp client host address')
 
-    def srp_client_add_service(self, instance_name, service_name, port, priority=0, weight=0, txt_entries=[]):
+    def srp_client_add_service(self,
+                               instance_name,
+                               service_name,
+                               port,
+                               priority=0,
+                               weight=0,
+                               txt_entries=[],
+                               lease=0,
+                               key_lease=0):
         txt_record = "".join(self._encode_txt_entry(entry) for entry in txt_entries)
-        self._cli_no_output('srp client service add', instance_name, service_name, port, priority, weight, txt_record)
+        self._cli_no_output('srp client service add', instance_name, service_name, port, priority, weight, txt_record,
+                            lease, key_lease)
 
     def srp_client_remove_service(self, instance_name, service_name):
         self._cli_no_output('srp client service remove', instance_name, service_name)
@@ -387,10 +581,10 @@
     # SRP server
 
     def srp_server_get_state(self):
-        return self._cli_single_output('srp server state', ['disabled', 'running', 'stopped'])
+        return self._cli_single_output('srp server state', expected_outputs=['disabled', 'running', 'stopped'])
 
     def srp_server_get_addr_mode(self):
-        return self._cli_single_output('srp server addrmode', ['unicast', 'anycast'])
+        return self._cli_single_output('srp server addrmode', expected_outputs=['unicast', 'anycast'])
 
     def srp_server_set_addr_mode(self, mode):
         self._cli_no_output('srp server addrmode', mode)
@@ -462,6 +656,8 @@
                'priority': '0',
                'weight': '0',
                'ttl': '7200',
+               'lease': '7200',
+               'key-lease', '1209600',
                'TXT': ['abc=010203'],
                'host_fullname': 'my-host.default.service.arpa.',
                'host': 'my-host',
@@ -482,8 +678,8 @@
             if service['deleted'] == 'true':
                 service_list.append(service)
                 continue
-            # 'subtypes', port', 'priority', 'weight', 'ttl'
-            for i in range(0, 5):
+            # 'subtypes', port', 'priority', 'weight', 'ttl', 'lease', 'key-lease'
+            for i in range(0, 7):
                 key_value = outputs.pop(0).strip().split(':')
                 service[key_value[0].strip()] = key_value[1].strip()
             txt_entries = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',')
@@ -520,6 +716,7 @@
             self.set_channel(channel)
         if xpanid is not None:
             self.set_ext_panid(xpanid)
+        self.set_mode('rdn')
         self.set_panid(panid)
         self.interface_up()
         self.thread_start()
@@ -534,6 +731,9 @@
             self.set_mode('rn')
         elif type == JOIN_TYPE_SLEEPY_END_DEVICE:
             self.set_mode('-')
+        elif type == JOIN_TYPE_REED:
+            self.set_mode('rdn')
+            self.set_router_eligible('disable')
         else:
             self.set_mode('rdn')
             self.set_router_selection_jitter(1)
@@ -553,6 +753,64 @@
         """Removes a given node (of node `Node) from the allowlist"""
         self._cli_no_output('macfilter addr remove', node.get_ext_addr())
 
+    def set_macfilter_lqi_to_node(self, node, lqi):
+        self._cli_no_output('macfilter rss add-lqi', node.get_ext_addr(), lqi)
+
+    # ------------------------------------------------------------------------------------------------------------------
+    # Radio nodeidfilter
+
+    def nodeidfilter_clear(self, node):
+        self._cli_no_output('nodeidfilter clear')
+
+    def nodeidfilter_allow(self, node):
+        self._cli_no_output('nodeidfilter allow', node.index)
+
+    def nodeidfilter_deny(self, node):
+        self._cli_no_output('nodeidfilter deny', node.index)
+
+    # ------------------------------------------------------------------------------------------------------------------
+    # Parsing helpers
+
+    @classmethod
+    def parse_table(cls, table_lines):
+        verify(len(table_lines) >= 2)
+        headers = cls.split_table_row(table_lines[0])
+        info = []
+        for row in table_lines[2:]:
+            if row.strip() == '':
+                continue
+            fields = cls.split_table_row(row)
+            verify(len(fields) == len(headers))
+            info.append({headers[i]: fields[i] for i in range(len(fields))})
+        return info
+
+    @classmethod
+    def split_table_row(cls, row):
+        return [field.strip() for field in row.strip().split('|')[1:-1]]
+
+    @classmethod
+    def parse_list(cls, list_lines):
+        result = {}
+        for line in list_lines:
+            fields = line.split(':', 1)
+            result[fields[0].strip()] = fields[1].strip()
+        return result
+
+    @classmethod
+    def parse_multiradio_neighbor_entry(cls, line):
+        # Example: "ExtAddr:42aa94ad67229f14, RLOC16:0x9400, Radios:[15.4(245), TREL(255)]"
+        result = {}
+        for field in line.split(', ', 2):
+            key_value = field.split(':')
+            result[key_value[0]] = key_value[1]
+        radios = {}
+        for item in result['Radios'][1:-1].split(','):
+            name, prf = item.strip().split('(')
+            verify(prf.endswith(')'))
+            radios[name] = int(prf[:-1])
+        result['Radios'] = radios
+        return result
+
     # ------------------------------------------------------------------------------------------------------------------
     # class methods
 
diff --git a/tests/toranj/cli/test-002-form.py b/tests/toranj/cli/test-002-form.py
index eea06d5..3bd3c16 100755
--- a/tests/toranj/cli/test-002-form.py
+++ b/tests/toranj/cli/test-002-form.py
@@ -40,7 +40,7 @@
 # -----------------------------------------------------------------------------------------------------------------------
 # Creating `Nodes` instances
 
-speedup = 4
+speedup = 10
 cli.Node.set_time_speedup_factor(speedup)
 
 node = cli.Node()
@@ -48,8 +48,6 @@
 # -----------------------------------------------------------------------------------------------------------------------
 # Test implementation
 
-WAIT_TIME = 5
-
 verify(node.get_state() == 'disabled')
 node.form('test')
 
diff --git a/tests/toranj/cli/test-003-join.py b/tests/toranj/cli/test-003-join.py
index f1cc05a..d24602a 100755
--- a/tests/toranj/cli/test-003-join.py
+++ b/tests/toranj/cli/test-003-join.py
@@ -40,14 +40,12 @@
 # -----------------------------------------------------------------------------------------------------------------------
 # Creating `cli.Nodes` instances
 
-speedup = 4
+speedup = 10
 cli.Node.set_time_speedup_factor(speedup)
 
 node1 = cli.Node()
 node2 = cli.Node()
 
-WAIT_TIME = 5
-
 # -----------------------------------------------------------------------------------------------------------------------
 # Test implementation
 
@@ -72,6 +70,20 @@
 verify(node2.get_state() == 'child')
 verify(node2.get_mode() == '-')
 
+node2.interface_down()
+
+# Create a poor link between child and parent using MAC fixed RSSI
+# filter and make sure child can still attach.
+
+node1.cli('macfilter rss add * -99')
+node2.cli('macfilter rss add * -99')
+
+node2.join(node1, cli.JOIN_TYPE_END_DEVICE)
+verify(node2.get_state() == 'child')
+verify(node2.get_mode() == 'rn')
+
+verify(len(node1.get_child_table()) == 1)
+
 # -----------------------------------------------------------------------------------------------------------------------
 # Test finished
 
diff --git a/tests/toranj/cli/test-004-scan.py b/tests/toranj/cli/test-004-scan.py
new file mode 100755
index 0000000..35590db
--- /dev/null
+++ b/tests/toranj/cli/test-004-scan.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test description: MAC scan operation
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+
+# -----------------------------------------------------------------------------------------------------------------------
+def verify_scan_result_conatins_nodes(scan_result, nodes):
+    table = cli.Node.parse_table(scan_result)
+    verify(len(table) >= len(nodes))
+    for node in nodes:
+        ext_addr = node.get_ext_addr()
+        for entry in table:
+            if entry['MAC Address'] == ext_addr:
+                verify(int(entry['PAN'], 16) == int(node.get_panid(), 16))
+                verify(int(entry['Ch']) == int(node.get_channel()))
+                break
+        else:
+            verify(False)
+
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+node1 = cli.Node()
+node2 = cli.Node()
+node3 = cli.Node()
+node4 = cli.Node()
+node5 = cli.Node()
+scanner = cli.Node()
+
+nodes = [node1, node2, node3, node4, node5]
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test implementation
+
+node1.form('net1', panid=0x1111, channel=12)
+node2.form('net2', panid=0x2222, channel=13)
+node3.form('net3', panid=0x3333, channel=14)
+node4.form('net4', panid=0x4444, channel=15)
+node5.form('net5', panid=0x5555, channel=16)
+
+# MAC scan
+
+# Scan on all channels, should see all nodes
+verify_scan_result_conatins_nodes(scanner.cli('scan'), nodes)
+
+# Scan on channel 12 only, should only see node1
+verify_scan_result_conatins_nodes(scanner.cli('scan 12'), [node1])
+
+# Scan on channel 20 only, should see no result
+verify_scan_result_conatins_nodes(scanner.cli('scan 20'), [])
+
+# MLE Discover scan
+
+scanner.interface_up()
+
+verify_scan_result_conatins_nodes(scanner.cli('discover'), nodes)
+verify_scan_result_conatins_nodes(scanner.cli('scan 12'), [node1])
+verify_scan_result_conatins_nodes(scanner.cli('scan 20'), [])
+
+scanner.form('scanner')
+verify_scan_result_conatins_nodes(scanner.cli('discover'), nodes)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-005-traffic-router-to-child.py b/tests/toranj/cli/test-005-traffic-router-to-child.py
new file mode 100755
index 0000000..ac00444
--- /dev/null
+++ b/tests/toranj/cli/test-005-traffic-router-to-child.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test description:
+#
+# Traffic between router to end-device (link-local and mesh-local IPv6 addresses).
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+node1 = cli.Node()
+node2 = cli.Node()
+node3 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test implementation
+
+node1.form('net')
+node2.join(node1, cli.JOIN_TYPE_END_DEVICE)
+node3.join(node1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+
+verify(node1.get_state() == 'leader')
+verify(node2.get_state() == 'child')
+verify(node3.get_state() == 'child')
+
+ll1 = node1.get_linklocal_ip_addr()
+ll2 = node2.get_linklocal_ip_addr()
+ll3 = node2.get_linklocal_ip_addr()
+
+ml1 = node1.get_mleid_ip_addr()
+ml2 = node2.get_mleid_ip_addr()
+ml3 = node2.get_mleid_ip_addr()
+
+sizes = [0, 80, 500, 1000]
+count = 2
+
+for size in sizes:
+    node1.ping(ll2, size=size, count=count)
+    node2.ping(ll1, size=size, count=count)
+    node1.ping(ml2, size=size, count=count)
+    node2.ping(ml1, size=size, count=count)
+
+poll_periods = [10, 100, 300]
+
+for period in poll_periods:
+    node3.set_pollperiod(period)
+    verify(int(node3.get_pollperiod()) == period)
+
+    for size in sizes:
+        node1.ping(ll3, size=size, count=count)
+        node3.ping(ll1, size=size, count=count)
+        node1.ping(ml3, size=size, count=count)
+        node3.ping(ml1, size=size, count=count)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-006-traffic-multi-hop.py b/tests/toranj/cli/test-006-traffic-multi-hop.py
new file mode 100755
index 0000000..0f6f48c
--- /dev/null
+++ b/tests/toranj/cli/test-006-traffic-multi-hop.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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:
+#
+# Traffic over multi-hop in a network with chain topology
+#
+#       r1 ----- r2 ---- r3 ----- r4
+#       /\                        /\
+#      /  \                      /  \
+#    fed1 sed1                sed4 fed4
+#
+#
+# Traffic flow:
+#  - From first router to last router
+#  - From SED child of last router to SED child of first router
+#  - From FED child of first router to FED child of last router
+#
+# The test verifies the following:
+# - Verifies Address Query process to routers and FEDs.
+# - Verifies Mesh Header frame forwarding over multiple routers.
+# - Verifies forwarding of large IPv6 messages (1000 bytes) requiring lowpan fragmentation.
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+r3 = cli.Node()
+r4 = cli.Node()
+fed1 = cli.Node()
+sed1 = cli.Node()
+fed4 = cli.Node()
+sed4 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+r1.allowlist_node(fed1)
+r1.allowlist_node(sed1)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+
+r3.allowlist_node(r2)
+r3.allowlist_node(r4)
+
+r4.allowlist_node(r3)
+r4.allowlist_node(sed4)
+r4.allowlist_node(fed4)
+
+fed1.allowlist_node(r1)
+sed1.allowlist_node(r1)
+
+fed4.allowlist_node(r4)
+sed4.allowlist_node(r4)
+
+r1.form("pan")
+r2.join(r1)
+r3.join(r2)
+r4.join(r3)
+fed1.join(r1, cli.JOIN_TYPE_REED)
+sed1.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+fed4.join(r4, cli.JOIN_TYPE_REED)
+sed4.join(r4, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(r3.get_state() == 'router')
+verify(r4.get_state() == 'router')
+verify(fed1.get_state() == 'child')
+verify(fed4.get_state() == 'child')
+verify(sed1.get_state() == 'child')
+verify(sed4.get_state() == 'child')
+
+sed1.set_pollperiod(200)
+sed4.set_pollperiod(200)
+verify(int(sed1.get_pollperiod()) == 200)
+verify(int(sed4.get_pollperiod()) == 200)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# Wait till first router has either established a link or
+# has a valid "next hop" towards all other routers.
+
+r1_rloc16 = int(r1.get_rloc16(), 16)
+
+
+def check_r1_router_table():
+    table = r1.get_router_table()
+    verify(len(table) == 4)
+    for entry in table:
+        verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63)
+
+
+verify_within(check_r1_router_table, 160)
+
+sizes = [0, 80, 500, 1000]
+
+# Traffic from the first router in the chain to the last one.
+
+r4_mleid = r4.get_mleid_ip_addr()
+
+for size in sizes:
+    r1.ping(r4_mleid, size=size)
+
+# Send from the SED child of the last router to the SED child of the first
+# router.
+
+sed1_mleid = sed1.get_mleid_ip_addr()
+
+for size in sizes:
+    sed4.ping(sed1_mleid, size=size)
+
+# Send from the FED child of the first router to the FED child of the last
+# router.
+
+fed4_mleid = fed4.get_mleid_ip_addr()
+
+for size in sizes:
+    fed1.ping(fed4_mleid, size=size)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-007-off-mesh-route-traffic.py b/tests/toranj/cli/test-007-off-mesh-route-traffic.py
new file mode 100755
index 0000000..bd72387
--- /dev/null
+++ b/tests/toranj/cli/test-007-off-mesh-route-traffic.py
@@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: Adding off-mesh routes (on routers and FEDs) and traffic flow to off-mesh addresses.
+#
+# Test topology:
+#
+#     r1---- r2 ---- r3 ---- r4
+#     |              |
+#     |              |
+#    fed1           med3
+#
+# The off-mesh-routes are added as follows:
+# - `r1`   adds fd00:1:1::/48   high
+# - `r2`   adds fd00:1:1:2::64  med
+# - `r3`   adds fd00:3::/64     med  & fed00:4::/32 med
+# - 'r4'   adds fd00:1:1::/48   low
+# - `fed1` adds fd00:4::/32     med
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+r3 = cli.Node()
+r4 = cli.Node()
+fed1 = cli.Node()
+med3 = cli.Node()
+
+nodes = [r1, r2, r3, r4, fed1, med3]
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+r1.allowlist_node(fed1)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+
+r3.allowlist_node(r2)
+r3.allowlist_node(r4)
+r3.allowlist_node(med3)
+
+r4.allowlist_node(r3)
+
+fed1.allowlist_node(r1)
+med3.allowlist_node(r3)
+
+r1.form("off-mesh")
+r2.join(r1)
+r3.join(r2)
+r4.join(r3)
+fed1.join(r1, cli.JOIN_TYPE_REED)
+med3.join(r3, cli.JOIN_TYPE_END_DEVICE)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(r3.get_state() == 'router')
+verify(r4.get_state() == 'router')
+verify(fed1.get_state() == 'child')
+verify(med3.get_state() == 'child')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# Wait till first router has either established a link or
+# has a valid "next hop" towards all other routers.
+
+r1_rloc16 = int(r1.get_rloc16(), 16)
+
+
+def check_r1_router_table():
+    table = r1.get_router_table()
+    verify(len(table) == 4)
+    for entry in table:
+        verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63)
+
+
+verify_within(check_r1_router_table, 120)
+
+# Add an on-mesh prefix on r1 and different off-mesh routes
+# on different nodes and make sure they are seen on all nodes
+
+r1.add_prefix('fd00:abba::/64', 'paros', 'med')
+r1.add_route('fd00:1:1::/48', 's', 'high')
+r1.register_netdata()
+
+r2.add_route('fd00:1:1:2::/64', 's', 'med')
+r2.register_netdata()
+
+r3.add_route('fd00:3::/64', 's', 'med')
+r3.add_route('fd00:4::/32', 'med')
+r3.register_netdata()
+
+r4.add_route('fd00:1:1::/48', 's', 'med')
+
+fed1.add_route('fd00:4::/32', 'med')
+fed1.register_netdata()
+
+
+def check_netdata_on_all_nodes():
+    for node in nodes:
+        netdata = node.get_netdata()
+        verify(len(netdata['prefixes']) == 1)
+        verify(len(netdata['routes']) == 6)
+
+
+verify_within(check_netdata_on_all_nodes, 5)
+
+# Send from `med3` to an address matching `fd00:1:1:2::/64` added
+# by`r2` and verify that it is received on `r2` and not `r1`, since
+# it is better prefix match with `fd00:1:1:2/64` on r2.
+
+r1_counter = r1.get_br_counter_unicast_outbound_packets()
+r2_counter = r2.get_br_counter_unicast_outbound_packets()
+
+med3.ping('fd00:1:1:2::1', verify_success=False)
+
+verify(r1_counter == r1.get_br_counter_unicast_outbound_packets())
+verify(r2_counter + 1 == r2.get_br_counter_unicast_outbound_packets())
+
+# Send from`r3` to an address matching `fd00:1:1::/48` which is
+# added by both `r1` and `r4` and verify that it is received on
+# `r1` since it adds it with higher preference.
+
+r1_counter = r1.get_br_counter_unicast_outbound_packets()
+r4_counter = r4.get_br_counter_unicast_outbound_packets()
+
+r3.ping('fd00:1:1::2', count=3, verify_success=False)
+
+verify(r1_counter + 3 == r1.get_br_counter_unicast_outbound_packets())
+verify(r4_counter == r4.get_br_counter_unicast_outbound_packets())
+
+# TRy the same address from `r4` itself, it should again end up
+# going to `r1` due to it adding it at high preference.
+
+r1_counter = r1.get_br_counter_unicast_outbound_packets()
+r4_counter = r4.get_br_counter_unicast_outbound_packets()
+
+r4.ping('fd00:1:1::2', count=2, verify_success=False)
+
+verify(r1_counter + 2 == r1.get_br_counter_unicast_outbound_packets())
+verify(r4_counter == r4.get_br_counter_unicast_outbound_packets())
+
+# Send from `fed1` to an address matching `fd00::3::/64` (from `r3`)
+# and verify that it is received on `r3`.
+
+r3_counter = r3.get_br_counter_unicast_outbound_packets()
+
+fed1.ping('fd00:3::3', count=2, verify_success=False)
+
+verify(r3_counter + 2 == r3.get_br_counter_unicast_outbound_packets())
+
+# Send from `r1` to an address matching `fd00::4::/32` which is added
+# by both `fed1` and `r3` with same preference. Verify that it is
+# received on `fed1` since it is closer to `r1` and we would have a
+# smaller path cost from `r1` to `fed1`.
+
+fed1_counter = fed1.get_br_counter_unicast_outbound_packets()
+r3_counter = r3.get_br_counter_unicast_outbound_packets()
+
+r1.ping('fd00:4::4', count=1, verify_success=False)
+
+verify(fed1_counter + 1 == fed1.get_br_counter_unicast_outbound_packets())
+verify(r3_counter == r3.get_br_counter_unicast_outbound_packets())
+
+# Try the same, but now send from `fed1` and make sure it selects
+# itself as destination.
+
+fed1_counter = fed1.get_br_counter_unicast_outbound_packets()
+r3_counter = r3.get_br_counter_unicast_outbound_packets()
+
+fed1.ping('fd00:4::5', count=2, verify_success=False)
+
+verify(fed1_counter + 2 == fed1.get_br_counter_unicast_outbound_packets())
+verify(r3_counter == r3.get_br_counter_unicast_outbound_packets())
+
+# Try the same but now send from `r2`. Now the `r3` would be closer
+# and should be selected as destination.
+
+fed1_counter = fed1.get_br_counter_unicast_outbound_packets()
+r3_counter = r3.get_br_counter_unicast_outbound_packets()
+
+r2.ping('fd00:4::6', count=3, verify_success=False)
+
+verify(fed1_counter == fed1.get_br_counter_unicast_outbound_packets())
+verify(r3_counter + 3 == r3.get_br_counter_unicast_outbound_packets())
+
+# Again try same but send from `med1` and make sure its parent
+# `r3` receives.
+
+fed1_counter = fed1.get_br_counter_unicast_outbound_packets()
+r3_counter = r3.get_br_counter_unicast_outbound_packets()
+
+med3.ping('fd00:4::7', count=1, verify_success=False)
+
+verify(fed1_counter == fed1.get_br_counter_unicast_outbound_packets())
+verify(r3_counter + 1 == r3.get_br_counter_unicast_outbound_packets())
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-008-multicast-traffic.py b/tests/toranj/cli/test-008-multicast-traffic.py
new file mode 100755
index 0000000..7623a9c
--- /dev/null
+++ b/tests/toranj/cli/test-008-multicast-traffic.py
@@ -0,0 +1,248 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: Multicast traffic
+#
+# Network topology
+#
+#     r1 ---- r2 ---- r3 ---- r4
+#             |               |
+#             |               |
+#            fed             sed
+#
+# Test covers the following multicast traffic:
+#
+# - r2  =>> link-local all-nodes.   Expected response from [r1, r3, fed].
+# - r3  =>> mesh-local all-nodes.   Expected response from [r1, r2, r4, fed].
+# - r3  =>> link-local all-routers. Expected response from [r2, r4].
+# - r3  =>> mesh-local all-routers. Expected response from all routers.
+# - r1  =>> link-local all-thread.  Expected response from [r1, r2].
+# - fed =>> mesh-local all-thread.  Expected response from all nodes.
+# - r1  =>> mesh-local all-thread (one hop). Expected response from [r2].
+# - r1  =>> mesh-local all-thread (two hops). Expected response from [r2, r3, fed].
+# - r1  =>> mesh-local all-thread (three hops). Expected response from [r2, r3, r4, fed].
+# - r1  =>> mesh-local all-thread (four hops). Expected response from [r2, r3, r4, fed, sed].
+# - r1  =>> specific address (on r2 and sed). Expected to receive on [r2, sed].
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+r3 = cli.Node()
+r4 = cli.Node()
+fed = cli.Node()
+sed = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(fed)
+r2.allowlist_node(r3)
+
+fed.allowlist_node(r2)
+
+r3.allowlist_node(r2)
+r3.allowlist_node(r4)
+
+r4.allowlist_node(r3)
+r4.allowlist_node(sed)
+
+sed.allowlist_node(r4)
+
+r1.form("multicast")
+r2.join(r1)
+r3.join(r2)
+r4.join(r3)
+fed.join(r1, cli.JOIN_TYPE_REED)
+
+sed.join(r3, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+
+sed.set_pollperiod(600)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(r3.get_state() == 'router')
+verify(r4.get_state() == 'router')
+verify(fed.get_state() == 'child')
+verify(sed.get_state() == 'child')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# Wait till first router has either established a link or
+# has a valid "next hop" towards all other routers.
+
+r1_rloc16 = int(r1.get_rloc16(), 16)
+
+
+def check_r1_router_table():
+    table = r1.get_router_table()
+    verify(len(table) == 4)
+    for entry in table:
+        verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63)
+
+
+verify_within(check_r1_router_table, 160)
+
+# r2  =>> link-local all-nodes. Expected response from [r1, r3, fed].
+
+outputs = r2.cli('ping ff02::1')
+verify(len(outputs) == 4)
+for node in [r1, r3, fed]:
+    ll_addr = node.get_linklocal_ip_addr()
+    verify(any(ll_addr in line for line in outputs))
+
+# r3  =>> mesh-local all-nodes. Expected response from [r1, r2, r4, fed].
+
+outputs = r3.cli('ping ff03::1')
+verify(len(outputs) == 5)
+for node in [r1, r2, r4, fed]:
+    ml_addr = node.get_mleid_ip_addr()
+    verify(any(ml_addr in line for line in outputs))
+
+# r3  =>> link-local all-routers. Expected response from [r2, r4].
+
+outputs = r3.cli('ping ff02::2')
+verify(len(outputs) == 3)
+for node in [r2, r4]:
+    ll_addr = node.get_linklocal_ip_addr()
+    verify(any(ll_addr in line for line in outputs))
+
+# r3  =>> mesh-local all-routers. Expected response from all routers.
+
+outputs = r3.cli('ping ff03::2')
+verify(len(outputs) == 5)
+for node in [r1, r2, r4, fed]:
+    ml_addr = node.get_mleid_ip_addr()
+    verify(any(ml_addr in line for line in outputs))
+
+# r1  =>> link-local all-thread.  Expected response from [r2].
+
+ml_prefix = r1.get_mesh_local_prefix().strip().split('/')[0]
+ll_all_thread_nodes_addr = 'ff32:40:' + ml_prefix + '1'
+outputs = r1.cli('ping', ll_all_thread_nodes_addr)
+verify(len(outputs) == 2)
+for node in [r2]:
+    ll_addr = node.get_linklocal_ip_addr()
+    verify(any(ll_addr in line for line in outputs))
+
+# fed =>> mesh-local all-thread.  Expected response from all nodes.
+
+ml_all_thread_nodes_addr = 'ff33:40:' + ml_prefix + '1'
+outputs = fed.cli('ping', ml_all_thread_nodes_addr)
+verify(len(outputs) == 6)
+for node in [r1, r2, r3, r4, sed]:
+    ml_addr = node.get_mleid_ip_addr()
+    verify(any(ml_addr in line for line in outputs))
+
+# Repeat the same with larger ping msg requiring fragmentation
+
+outputs = fed.cli('ping', ml_all_thread_nodes_addr, 200)
+verify(len(outputs) == 6)
+for node in [r1, r2, r3, r4, sed]:
+    ml_addr = node.get_mleid_ip_addr()
+    verify(any(ml_addr in line for line in outputs))
+
+# r1  =>> mesh-local all-thread (one hop). Expected response from [r2].
+
+hop_limit = 1
+outputs = r1.cli('ping', ml_all_thread_nodes_addr, 10, 1, 0, hop_limit)
+verify(len(outputs) == 2)
+for node in [r2]:
+    ml_addr = node.get_mleid_ip_addr()
+    verify(any(ml_addr in line for line in outputs))
+
+# r1  =>> mesh-local all-thread (two hops). Expected response from [r2, r3, fed].
+
+hop_limit = 2
+outputs = r1.cli('ping', ml_all_thread_nodes_addr, 10, 1, 0, hop_limit)
+verify(len(outputs) == 4)
+for node in [r2, r3, fed]:
+    ml_addr = node.get_mleid_ip_addr()
+    verify(any(ml_addr in line for line in outputs))
+
+# r1  =>> mesh-local all-thread (three hops). Expected response from [r2, r3, r4, fed].
+
+hop_limit = 3
+outputs = r1.cli('ping', ml_all_thread_nodes_addr, 10, 1, 0, hop_limit)
+verify(len(outputs) == 5)
+for node in [r2, r3, r4, fed]:
+    ml_addr = node.get_mleid_ip_addr()
+    verify(any(ml_addr in line for line in outputs))
+
+# r1  =>> mesh-local all-thread (four hops). Expected response from [r2, r3, r4, fed, sed].
+
+hop_limit = 4
+outputs = r1.cli('ping', ml_all_thread_nodes_addr, 10, 1, 0, hop_limit)
+verify(len(outputs) == 6)
+for node in [r2, r3, r4, fed, sed]:
+    ml_addr = node.get_mleid_ip_addr()
+    verify(any(ml_addr in line for line in outputs))
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Subscribe to a specific multicast address on r2 and sed
+
+mcast_addr = 'ff03:0:0:0:0:0:0:114'
+r2.add_ip_maddr(mcast_addr)
+sed.add_ip_maddr(mcast_addr)
+time.sleep(1)
+maddrs = r2.get_ip_maddrs()
+verify(any(mcast_addr in maddr for maddr in maddrs))
+maddrs = sed.get_ip_maddrs()
+verify(any(mcast_addr in maddr for maddr in maddrs))
+
+outputs = r1.cli('ping', mcast_addr)
+verify(len(outputs) == 3)
+for node in [r2, sed]:
+    ml_addr = node.get_mleid_ip_addr()
+    verify(any(ml_addr in line for line in outputs))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-009-router-table.py b/tests/toranj/cli/test-009-router-table.py
new file mode 100755
index 0000000..67159f8
--- /dev/null
+++ b/tests/toranj/cli/test-009-router-table.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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:
+#
+# Verify router table entries.
+#
+#     r1 ------ r2 ---- r6
+#      \        /
+#       \      /
+#        \    /
+#          r3 ------ r4 ----- r5
+#
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+r3 = cli.Node()
+r4 = cli.Node()
+r5 = cli.Node()
+r6 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+r1.allowlist_node(r3)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+r2.allowlist_node(r6)
+
+r3.allowlist_node(r1)
+r3.allowlist_node(r2)
+r3.allowlist_node(r4)
+
+r4.allowlist_node(r3)
+r4.allowlist_node(r5)
+
+r5.allowlist_node(r4)
+
+r6.allowlist_node(r2)
+
+r1.form("topo")
+for node in [r2, r3, r4, r5, r6]:
+    node.join(r1)
+
+verify(r1.get_state() == 'leader')
+for node in [r2, r3, r4, r5, r6]:
+    verify(node.get_state() == 'router')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+r1_rloc16 = int(r1.get_rloc16(), 16)
+r2_rloc16 = int(r2.get_rloc16(), 16)
+r3_rloc16 = int(r3.get_rloc16(), 16)
+r4_rloc16 = int(r4.get_rloc16(), 16)
+r5_rloc16 = int(r5.get_rloc16(), 16)
+r6_rloc16 = int(r6.get_rloc16(), 16)
+
+r1_rid = r1_rloc16 / 1024
+r2_rid = r2_rloc16 / 1024
+r3_rid = r3_rloc16 / 1024
+r4_rid = r4_rloc16 / 1024
+r5_rid = r5_rloc16 / 1024
+r6_rid = r6_rloc16 / 1024
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def check_r1_router_table():
+    table = r1.get_router_table()
+    verify(len(table) == 6)
+    for entry in table:
+        rloc16 = int(entry['RLOC16'], 0)
+        link = int(entry['Link'])
+        nexthop = int(entry['Next Hop'])
+        cost = int(entry['Path Cost'])
+        if rloc16 == r1_rloc16:
+            verify(link == 0)
+        elif rloc16 == r2_rloc16:
+            verify(link == 1)
+            verify(nexthop == r3_rid)
+        elif rloc16 == r3_rloc16:
+            verify(link == 1)
+            verify(nexthop == r2_rid)
+        elif rloc16 == r4_rloc16:
+            verify(link == 0)
+            verify(nexthop == r3_rid)
+            verify(cost == 1)
+        elif rloc16 == r5_rloc16:
+            verify(link == 0)
+            verify(nexthop == r3_rid)
+            verify(cost == 2)
+        elif rloc16 == r6_rloc16:
+            verify(link == 0)
+            verify(nexthop == r2_rid)
+            verify(cost == 1)
+
+
+verify_within(check_r1_router_table, 160)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def check_r2_router_table():
+    table = r2.get_router_table()
+    verify(len(table) == 6)
+    for entry in table:
+        rloc16 = int(entry['RLOC16'], 0)
+        link = int(entry['Link'])
+        nexthop = int(entry['Next Hop'])
+        cost = int(entry['Path Cost'])
+        if rloc16 == r1_rloc16:
+            verify(link == 1)
+            verify(nexthop == r3_rid)
+        elif rloc16 == r2_rloc16:
+            verify(link == 0)
+        elif rloc16 == r3_rloc16:
+            verify(link == 1)
+            verify(nexthop == r1_rid)
+        elif rloc16 == r4_rloc16:
+            verify(link == 0)
+            verify(nexthop == r3_rid)
+            verify(cost == 1)
+        elif rloc16 == r5_rloc16:
+            verify(link == 0)
+            verify(nexthop == r3_rid)
+            verify(cost == 2)
+        elif rloc16 == r6_rloc16:
+            verify(link == 1)
+
+
+verify_within(check_r2_router_table, 160)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def check_r4_router_table():
+    table = r4.get_router_table()
+    verify(len(table) == 6)
+    for entry in table:
+        rloc16 = int(entry['RLOC16'], 0)
+        link = int(entry['Link'])
+        nexthop = int(entry['Next Hop'])
+        cost = int(entry['Path Cost'])
+        if rloc16 == r1_rloc16:
+            verify(link == 0)
+            verify(nexthop == r3_rid)
+            verify(cost == 1)
+        elif rloc16 == r2_rloc16:
+            verify(link == 0)
+            verify(nexthop == r3_rid)
+            verify(cost == 1)
+        elif rloc16 == r3_rloc16:
+            verify(link == 1)
+        elif rloc16 == r4_rloc16:
+            verify(link == 0)
+        elif rloc16 == r5_rloc16:
+            verify(link == 1)
+        elif rloc16 == r6_rloc16:
+            verify(link == 0)
+            verify(nexthop == r3_rid)
+            verify(cost == 2)
+
+
+verify_within(check_r4_router_table, 160)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def check_r5_router_table():
+    table = r5.get_router_table()
+    verify(len(table) == 6)
+    for entry in table:
+        rloc16 = int(entry['RLOC16'], 0)
+        link = int(entry['Link'])
+        nexthop = int(entry['Next Hop'])
+        cost = int(entry['Path Cost'])
+        if rloc16 == r1_rloc16:
+            verify(link == 0)
+            verify(nexthop == r4_rid)
+            verify(cost == 2)
+        elif rloc16 == r2_rloc16:
+            verify(link == 0)
+            verify(nexthop == r4_rid)
+            verify(cost == 2)
+        elif rloc16 == r3_rloc16:
+            verify(link == 0)
+            verify(nexthop == r4_rid)
+            verify(cost == 1)
+        elif rloc16 == r4_rloc16:
+            verify(link == 1)
+        elif rloc16 == r5_rloc16:
+            verify(link == 0)
+        elif rloc16 == r6_rloc16:
+            verify(link == 0)
+            verify(nexthop == r4_rid)
+            verify(cost == 3)
+
+
+verify_within(check_r5_router_table, 160)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-010-partition-merge.py b/tests/toranj/cli/test-010-partition-merge.py
new file mode 100755
index 0000000..4153737
--- /dev/null
+++ b/tests/toranj/cli/test-010-partition-merge.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: Partition formation and merge
+#
+# Network Topology:
+#
+#      r1 ---- / ---- r2
+#      |       \      |
+#      |       /      |
+#      c1      \      c2
+#
+#
+# Test covers the following situations:
+#
+# - r2 forming its own partition when it can no longer hear r1
+# - Partitions merging into one once r1 and r2 can talk again
+# - Adding on-mesh prefixes on each partition and ensuring after
+#   merge the info in combined.
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 25
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+c1 = cli.Node()
+c2 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+r1.allowlist_node(c1)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(c2)
+
+r1.form("partmrge")
+r2.join(r1)
+c1.join(r1, cli.JOIN_TYPE_END_DEVICE)
+c2.join(r2, cli.JOIN_TYPE_END_DEVICE)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(c1.get_state() == 'child')
+verify(c2.get_state() == 'child')
+
+nodes = [r1, r2, c1, c2]
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# Force the two routers to form their own partition
+# by removing them from each other's allowlist table
+
+r1.un_allowlist_node(r2)
+r2.un_allowlist_node(r1)
+
+# Add a prefix before r2 realizes it can no longer talk
+# to leader (r1).
+
+r2.add_prefix('fd00:abba::/64', 'paros', 'med')
+r2.register_netdata()
+
+# Check that r2 forms its own partition
+
+
+def check_r2_become_leader():
+    verify(r2.get_state() == 'leader')
+
+
+verify_within(check_r2_become_leader, 20)
+
+# While we have two partition, add a prefix on r1
+r1.add_prefix('fd00:1234::/64', 'paros', 'med')
+r1.register_netdata()
+
+# Update allowlist and wait for partitions to merge.
+r1.allowlist_node(r2)
+r2.allowlist_node(r1)
+
+
+def check_partition_id_match():
+    verify(r1.get_partition_id() == r2.get_partition_id())
+
+
+verify_within(check_partition_id_match, 20)
+
+# Check that partitions merged successfully
+
+
+def check_r1_r2_roles():
+    verify(r1.get_state() in ['leader', 'router'])
+    verify(r2.get_state() in ['leader', 'router'])
+
+
+verify_within(check_r1_r2_roles, 10)
+
+# Verify all nodes see both prefixes
+
+
+def check_netdata_on_all_nodes():
+    for node in nodes:
+        netdata = node.get_netdata()
+        verify(len(netdata['prefixes']) == 2)
+
+
+verify_within(check_netdata_on_all_nodes, 10)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-011-network-data-timeout.py b/tests/toranj/cli/test-011-network-data-timeout.py
new file mode 100755
index 0000000..a3a4086
--- /dev/null
+++ b/tests/toranj/cli/test-011-network-data-timeout.py
@@ -0,0 +1,141 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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 Data (on-mesh prefix) timeout and entry removal
+#
+# Network topology
+#
+#   r1 ----- r2
+#            |
+#            |
+#            c2 (sleepy)
+#
+#
+# Test covers the following steps:
+#
+# - Every node adds a unique on-mesh prefix.
+# - Every node also adds a common on-mesh prefix (with different flags).
+# - Verify that all the unique and common prefixes are present on all nodes are associated with correct RLOC16.
+# - Remove `r2` from network (which removes `c2` as well) from Thread partition created by `r1`.
+# - Verify that all on-mesh prefixes added by `r2` or `c2` (unique and common) are removed on `r1`.
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 25
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+c2 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(c2)
+
+c2.allowlist_node(r2)
+
+r1.form("netdatatmout")
+r2.join(r1)
+c2.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+c2.set_pollperiod(500)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(c2.get_state() == 'child')
+
+nodes = [r1, r2, c2]
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+common_prefix = 'fd00:cafe::'
+prefix1 = 'fd00:1::'
+prefix2 = 'fd00:2::'
+prefix3 = 'fd00:3::'
+
+# Each node adds its own prefix.
+r1.add_prefix('fd00:1::/64', 'paros', 'med')
+r2.add_prefix('fd00:2::/64', 'paros', 'med')
+c2.add_prefix('fd00:3::/64', 'paros', 'med')
+
+# a common prefix is added by all three nodes (with different preference)
+r1.add_prefix('fd00:abba::/64', 'paros', 'high')
+r2.add_prefix('fd00:abba::/64', 'paros', 'med')
+c2.add_prefix('fd00:abba::/64', 'paros', 'low')
+
+r1.register_netdata()
+r2.register_netdata()
+c2.register_netdata()
+
+
+def check_netdata_on_all_nodes():
+    for node in nodes:
+        netdata = node.get_netdata()
+        prefixes = netdata['prefixes']
+        verify(len(prefixes) == 6)
+
+
+verify_within(check_netdata_on_all_nodes, 10)
+
+# Remove `r2`. This should trigger all the prefixes added by it or its
+# child to timeout and be removed.
+
+r2.thread_stop()
+r2.interface_down()
+
+
+def check_netdata_on_r1():
+    netdata = r1.get_netdata()
+    prefixes = netdata['prefixes']
+    verify(len(prefixes) == 2)
+
+
+verify_within(check_netdata_on_r1, 120)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-012-reset-recovery.py b/tests/toranj/cli/test-012-reset-recovery.py
new file mode 100755
index 0000000..ea92a43
--- /dev/null
+++ b/tests/toranj/cli/test-012-reset-recovery.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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:
+# This test covers reset of parent, router, leader and restoring children after reset
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 25
+cli.Node.set_time_speedup_factor(speedup)
+
+leader = cli.Node()
+router = cli.Node()
+child1 = cli.Node()
+child2 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+leader.form('reset')
+child1.join(leader, cli.JOIN_TYPE_REED)
+child2.join(leader, cli.JOIN_TYPE_END_DEVICE)
+
+verify(leader.get_state() == 'leader')
+verify(child1.get_state() == 'child')
+verify(child2.get_state() == 'child')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Reset the parent and verify that both children are restored on
+# parent through "Child Update" exchange process and none of them got
+# detached and needed to attach back.
+
+del leader
+leader = cli.Node(index=1)
+leader.interface_up()
+leader.thread_start()
+
+
+def check_leader_state():
+    verify(leader.get_state() == 'leader')
+
+
+verify_within(check_leader_state, 10)
+
+# Check that `child1` and `child2` did not detach
+
+verify(child1.get_state() == 'child')
+verify(child2.get_state() == 'child')
+
+verify(int(cli.Node.parse_list(child1.get_mle_counter())['Role Detached']) == 1)
+verify(int(cli.Node.parse_list(child2.get_mle_counter())['Role Detached']) == 1)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Reset `router` and make sure it recovers as router with same router ID.
+
+router.join(leader)
+
+verify(router.get_state() == 'router')
+router_rloc16 = int(router.get_rloc16(), 16)
+
+time.sleep(0.75)
+
+del router
+router = cli.Node(index=2)
+router.interface_up()
+router.thread_start()
+
+
+def check_router_state():
+    verify(router.get_state() == 'router')
+
+
+verify_within(check_router_state, 10)
+verify(router_rloc16 == int(router.get_rloc16(), 16))
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Reset `leader` and make sure `router` is its neighbor again
+
+del leader
+leader = cli.Node(index=1)
+leader.interface_up()
+leader.thread_start()
+
+
+def check_leader_state():
+    verify(leader.get_state() == 'leader')
+
+
+verify_within(check_leader_state, 10)
+
+
+def check_leader_neighbor_table():
+    verify(len(leader.get_neighbor_table()) == 3)
+
+
+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
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-013-address-cache-parent-switch.py b/tests/toranj/cli/test-013-address-cache-parent-switch.py
new file mode 100755
index 0000000..eee1bef
--- /dev/null
+++ b/tests/toranj/cli/test-013-address-cache-parent-switch.py
@@ -0,0 +1,232 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: Address Cache Table
+#
+# This test verifies the behavior of `AddressResolver` module and entries in
+# address cache table. It also tests the behavior of nodes when there are
+# topology changes in the network (e.g., a child switches parent). In
+# particular, the test covers the address cache update through snooping, i.e.,
+# the logic which inspects forwarded frames to update address cache table if
+# source RLOC16 on a received frame differs from an existing entry in the
+# address cache table.
+#
+# Network topology:
+#
+#     r1 ---- r2 ---- r3
+#     |       |       |
+#     |       |       |
+#     c1      c2(s)   c3
+#
+# c1 and c3 are FED children, c2 is an SED which is first attached to r2 and
+# then forced to switch to r3.
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Nodes` instances
+
+speedup = 25
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+r3 = cli.Node()
+c1 = cli.Node()
+c2 = cli.Node()
+c3 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+r1.allowlist_node(c1)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+r2.allowlist_node(c2)
+
+r3.allowlist_node(r2)
+r3.allowlist_node(c3)
+
+c1.allowlist_node(r1)
+c2.allowlist_node(r2)
+c3.allowlist_node(r3)
+
+r1.form('addrrslvr')
+r2.join(r1)
+r3.join(r1)
+c1.join(r1, cli.JOIN_TYPE_REED)
+c2.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+c3.join(r1, cli.JOIN_TYPE_REED)
+c2.set_pollperiod(400)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(r3.get_state() == 'router')
+verify(c1.get_state() == 'child')
+verify(c2.get_state() == 'child')
+verify(c3.get_state() == 'child')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# Wait till first router has either established a link or
+# has a valid "next hop" towards all other routers.
+
+r1_rloc16 = int(r1.get_rloc16(), 16)
+
+
+def check_r1_router_table():
+    table = r1.get_router_table()
+    verify(len(table) == 3)
+    for entry in table:
+        verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63)
+
+
+verify_within(check_r1_router_table, 120)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+r1_address = r1.get_mleid_ip_addr()
+c1_address = c1.get_mleid_ip_addr()
+c2_address = c2.get_mleid_ip_addr()
+c3_address = c3.get_mleid_ip_addr()
+
+r1_rloc16 = int(r1.get_rloc16(), 16)
+r2_rloc16 = int(r2.get_rloc16(), 16)
+r3_rloc16 = int(r3.get_rloc16(), 16)
+c1_rloc16 = int(c1.get_rloc16(), 16)
+c3_rloc16 = int(c3.get_rloc16(), 16)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+#  From r1 ping c2 and c3
+
+r1.ping(c2_address)
+r1.ping(c3_address)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Verify that address cache table contains both c2 and c3 addresses c2
+# address should match its parent r2 (since c2 is sleepy), and c1
+# address should match to itself (since c3 is fed).
+
+cache_table = r1.get_eidcache()
+verify(len(cache_table) >= 2)
+
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    verify(fields[2] == 'cache')
+    if fields[0] == c2_address:
+        verify(int(fields[1], 16) == r2_rloc16)
+    elif fields[0] == c3_address:
+        verify(int(fields[1], 16) == c3_rloc16)
+    else:
+        verify(False)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Force c2 to switch its parent from r2 to r3
+#
+# New network topology
+#
+#     r1 ---- r2 ---- r3
+#     |               /\
+#     |              /  \
+#     c1           c2(s) c3
+
+r2.un_allowlist_node(c2)
+r3.allowlist_node(c2)
+c2.allowlist_node(r3)
+
+c2.thread_stop()
+c2.thread_start()
+
+
+def check_c2_attaches_to_r3():
+    verify(c2.get_state() == 'child')
+    verify(int(c2.get_parent_info()['Rloc'], 16) == r3_rloc16)
+
+
+verify_within(check_c2_attaches_to_r3, 10)
+
+
+def check_r2_child_table_is_empty():
+    verify(len(r2.get_child_table()) == 0)
+
+
+verify_within(check_r2_child_table_is_empty, 10)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Note that r1 still has r2 as the destination for c2's address in its
+# address cache table. But since r2 is aware that c2 is no longer its
+# child, when it receives the IPv6 message with c2's address, r2
+# itself would do an address query for the address and forward the
+# IPv6 message.
+
+cache_table = r1.get_eidcache()
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    if fields[0] == c2_address:
+        verify(int(fields[1], 16) == r2_rloc16)
+        break
+else:
+    verify(False)
+
+r1.ping(c2_address)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Ping c1 from c2. This will go through c1's parent r1. r1 upon
+# receiving and forwarding the message should update its address
+# cache table for c2 (address cache update through snooping).
+
+c2.ping(c1_address)
+
+cache_table = r1.get_eidcache()
+
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    if fields[0] == c2_address:
+        verify(int(fields[1], 16) == r3_rloc16)
+        verify(fields[2] == 'snoop')
+        break
+else:
+    verify(False)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-014-address-resolver.py b/tests/toranj/cli/test-014-address-resolver.py
new file mode 100755
index 0000000..4203129
--- /dev/null
+++ b/tests/toranj/cli/test-014-address-resolver.py
@@ -0,0 +1,415 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: Address Cache Table
+#
+# This test verifies the behavior of `AddressResolver` and how the cache
+# table is managed. In particular it verifies behavior query timeout and
+# query retry and snoop optimization.
+#
+# Build network topology
+#
+#  r3 ---- r1 ---- r2
+#  |               |
+#  |               |
+#  c3              c2
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+r3 = cli.Node()
+c2 = cli.Node()
+c3 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+r1.allowlist_node(r3)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(c2)
+
+r3.allowlist_node(r1)
+r3.allowlist_node(c3)
+
+c2.allowlist_node(r2)
+c3.allowlist_node(r3)
+
+r1.form('addrrslvr')
+
+prefix = 'fd00:abba::'
+r1.add_prefix(prefix + '/64', 'pos', 'med')
+r1.register_netdata()
+
+r2.join(r1)
+r3.join(r1)
+c2.join(r1, cli.JOIN_TYPE_END_DEVICE)
+c3.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+c3.set_pollperiod(400)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(r3.get_state() == 'router')
+verify(c2.get_state() == 'child')
+verify(c3.get_state() == 'child')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# Wait till first router has either established a link or
+# has a valid "next hop" towards all other routers.
+
+r1_rloc16 = int(r1.get_rloc16(), 16)
+
+
+def check_r1_router_table():
+    table = r1.get_router_table()
+    verify(len(table) == 3)
+    for entry in table:
+        verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63)
+
+
+verify_within(check_r1_router_table, 120)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+r1_rloc = int(r1.get_rloc16(), 16)
+r2_rloc = int(r2.get_rloc16(), 16)
+r3_rloc = int(r3.get_rloc16(), 16)
+c2_rloc = int(c2.get_rloc16(), 16)
+c3_rloc = int(c3.get_rloc16(), 16)
+
+# AddressResolver constants:
+
+max_cache_entries = 16
+max_snooped_non_evictable = 2
+
+# Add IPv6 addresses matching the on-mesh prefix on all nodes
+
+r1.add_ip_addr(prefix + '1')
+
+num_addresses = 4  # Number of addresses to add on r2, r3, c2, and c3
+
+for num in range(num_addresses):
+    r2.add_ip_addr(prefix + "2:" + str(num))
+    r3.add_ip_addr(prefix + "3:" + str(num))
+    c2.add_ip_addr(prefix + "c2:" + str(num))
+    c3.add_ip_addr(prefix + "c3:" + str(num))
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# From r1 send msg to a group of addresses that are not provided by
+# any nodes in network.
+
+num_queries = 5
+stagger_interval = 1.2
+port = 1234
+initial_retry_delay = 8
+
+r1.udp_open()
+
+for num in range(num_queries):
+    r1.udp_send(prefix + '800:' + str(num), port, 'hi_nobody')
+    # Wait before next tx to stagger the address queries
+    # request ensuring different timeouts
+    time.sleep(stagger_interval / (num_queries * speedup))
+
+# Verify that we do see entries in cache table for all the addresses
+# and all are in "query" state
+
+cache_table = r1.get_eidcache()
+verify(len(cache_table) == num_queries)
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    verify(fields[2] == 'query')
+    verify(fields[3] == 'canEvict=0')
+    verify(fields[4].startswith('timeout='))
+    verify(int(fields[4].split('=')[1]) > 0)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Check the retry-query behavior
+#
+# Wait till all the address queries time out and verify they
+# enter "retry-query" state.
+
+
+def check_cache_entry_switch_to_retry_state():
+    cache_table = r1.get_eidcache()
+    for entry in cache_table:
+        fields = entry.strip().split(' ')
+        verify(fields[2] == 'retry')
+        verify(fields[3] == 'canEvict=1')
+        verify(fields[4].startswith('timeout='))
+        verify(int(fields[4].split('=')[1]) >= 0)
+        verify(fields[5].startswith('retryDelay='))
+        verify(int(fields[5].split('=')[1]) == initial_retry_delay)
+
+
+verify_within(check_cache_entry_switch_to_retry_state, 20)
+
+# Try sending again to same addresses which are all in "retry" state.
+
+for num in range(num_queries):
+    r1.udp_send(prefix + '800:' + str(num), port, 'hi_nobody')
+
+# Make sure the entries stayed in retry-query state as before.
+
+verify_within(check_cache_entry_switch_to_retry_state, 20)
+
+# Now wait for all entries to reach zero timeout.
+
+
+def check_cache_entry_in_retry_state_to_get_to_zero_timeout():
+    cache_table = r1.get_eidcache()
+    for entry in cache_table:
+        fields = entry.strip().split(' ')
+        verify(fields[2] == 'retry')
+        verify(fields[3] == 'canEvict=1')
+        verify(fields[4].startswith('timeout='))
+        verify(int(fields[4].split('=')[1]) == 0)
+
+
+verify_within(check_cache_entry_in_retry_state_to_get_to_zero_timeout, 20)
+
+# Now send again to the same addresses.
+
+for num in range(num_queries):
+    r1.udp_send(prefix + '800:' + str(num), port, 'hi_nobody')
+
+# We expect now after the delay to see retries for same addresses.
+
+
+def check_cache_entry_switch_to_query_state():
+    cache_table = r1.get_eidcache()
+    for entry in cache_table:
+        fields = entry.strip().split(' ')
+        verify(fields[2] == 'query')
+        verify(fields[3] == 'canEvict=1')
+
+
+verify_within(check_cache_entry_switch_to_query_state, 20)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Verify snoop optimization behavior.
+
+# Send to r1 from all addresses on r2.
+
+r2.udp_open()
+for num in range(num_addresses):
+    r2.udp_bind(prefix + '2:' + str(num), port)
+    r2.udp_send(prefix + '1', port, 'hi_r1_from_r2_snoop_me')
+
+# Verify that we see all addresses from r2 as snooped in cache table.
+# At most two of them should be marked as non-evictable.
+
+
+def check_cache_entry_contains_snooped_entries():
+    cache_table = r1.get_eidcache()
+    verify(len(cache_table) >= num_addresses)
+    snooped_count = 0
+    snooped_non_evictable = 0
+    for entry in cache_table:
+        fields = entry.strip().split(' ')
+        if fields[2] == 'snoop':
+            verify(fields[0].startswith('fd00:abba:0:0:0:0:2:'))
+            verify(int(fields[1], 16) == r2_rloc)
+            snooped_count = snooped_count + 1
+            if fields[3] == 'canEvict=0':
+                snooped_non_evictable = snooped_non_evictable + 1
+    verify(snooped_count == num_addresses)
+    verify(snooped_non_evictable == max_snooped_non_evictable)
+
+
+verify_within(check_cache_entry_contains_snooped_entries, 20)
+
+# Now we use the snooped entries by sending from r1 to r2 using
+# all its addresses.
+
+for num in range(num_addresses):
+    r1.udp_send(prefix + '2:' + str(num), port, 'hi_back_r2_from_r1')
+
+time.sleep(0.1)
+
+# We expect to see the entries to be in "cached" state now.
+
+cache_table = r1.get_eidcache()
+verify(len(cache_table) >= num_addresses)
+match_count = 0
+for entry in cache_table:
+    fields = entry.strip().split(' ')
+    if fields[0].startswith('fd00:abba:0:0:0:0:2:'):
+        verify(fields[2] == 'cache')
+        verify(fields[3] == 'canEvict=1')
+        match_count = match_count + 1
+verify(match_count == num_addresses)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Check query requests and last transaction time
+
+# Send from r1 to all addresses on r3. Check entries
+# for r3 are at the top of cache table list.
+
+for num in range(num_addresses):
+    r1.udp_send(prefix + '3:' + str(num), port, 'hi_r3_from_r1')
+
+
+def check_cache_entry_contains_r3_entries():
+    cache_table = r1.get_eidcache()
+    for num in range(num_addresses):
+        entry = cache_table[num]
+        fields = entry.strip().split(' ')
+        verify(fields[0].startswith('fd00:abba:0:0:0:0:3:'))
+        verify(int(fields[1], 16) == r3_rloc)
+        verify(fields[2] == 'cache')
+        verify(fields[3] == 'canEvict=1')
+        verify(fields[4] == 'transTime=0')
+
+
+verify_within(check_cache_entry_contains_r3_entries, 20)
+
+# Send from r1 to all addresses of c3 (sleepy child of r3)
+
+for num in range(num_addresses):
+    r1.udp_send(prefix + 'c3:' + str(num), port, 'hi_c3_from_r1')
+
+
+def check_cache_entry_contains_c3_entries():
+    cache_table = r1.get_eidcache()
+    for num in range(num_addresses):
+        entry = cache_table[num]
+        fields = entry.strip().split(' ')
+        verify(fields[0].startswith('fd00:abba:0:0:0:0:c3:'))
+        verify(int(fields[1], 16) == r3_rloc)
+        verify(fields[2] == 'cache')
+        verify(fields[3] == 'canEvict=1')
+        verify(fields[4] == 'transTime=0')
+
+
+verify_within(check_cache_entry_contains_c3_entries, 20)
+
+# Send again to r2. This should cause the related cache entries to
+# be moved to top of the list.
+
+for num in range(num_addresses):
+    r1.udp_send(prefix + '2:' + str(num), port, 'hi_again_r2_from_r1')
+
+
+def check_cache_entry_contains_r2_entries():
+    cache_table = r1.get_eidcache()
+    for num in range(num_addresses):
+        entry = cache_table[num]
+        fields = entry.strip().split(' ')
+        verify(fields[0].startswith('fd00:abba:0:0:0:0:2:'))
+        verify(int(fields[1], 16) == r2_rloc)
+        verify(fields[2] == 'cache')
+        verify(fields[3] == 'canEvict=1')
+
+
+verify_within(check_cache_entry_contains_r2_entries, 20)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Check behavior when address cache table is full.
+
+cache_table = r1.get_eidcache()
+verify(len(cache_table) == max_cache_entries)
+
+# From r1 send to non-existing addresses.
+
+for num in range(num_queries):
+    r1.udp_send(prefix + '900:' + str(num), port, 'hi_nobody!')
+
+cache_table = r1.get_eidcache()
+verify(len(cache_table) == max_cache_entries)
+
+# Send from c2 to r1 and verify that snoop optimization uses at most
+# `max_snooped_non_evictable` entries
+
+c2.udp_open()
+
+for num in range(num_addresses):
+    c2.udp_bind(prefix + 'c2:' + str(num), port)
+    c2.udp_send(prefix + '1', port, 'hi_r1_from_c2_snoop_me')
+
+
+def check_cache_entry_contains_max_allowed_snopped():
+    cache_table = r1.get_eidcache()
+    snooped_non_evictable = 0
+    for entry in cache_table:
+        fields = entry.strip().split(' ')
+        if fields[2] == 'snoop':
+            verify(fields[0].startswith('fd00:abba:0:0:0:0:c2:'))
+            verify(fields[3] == 'canEvict=0')
+            snooped_non_evictable = snooped_non_evictable + 1
+    verify(snooped_non_evictable == max_snooped_non_evictable)
+
+
+verify_within(check_cache_entry_contains_max_allowed_snopped, 20)
+
+# Now send from r1 to c2, the snooped entries would be used
+# some other addresses will  go through full address query.
+
+for num in range(num_addresses):
+    r1.udp_send(prefix + 'c2:' + str(num), port, 'hi_c2_from_r1')
+
+
+def check_cache_entry_contains_c2_entries():
+    cache_table = r1.get_eidcache()
+    for num in range(num_addresses):
+        entry = cache_table[num]
+        fields = entry.strip().split(' ')
+        verify(fields[0].startswith('fd00:abba:0:0:0:0:c2:'))
+        verify(int(fields[1], 16) == r2_rloc)
+        verify(fields[2] == 'cache')
+        verify(fields[3] == 'canEvict=1')
+
+
+verify_within(check_cache_entry_contains_c2_entries, 20)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-015-clear-addresss-cache-for-sed.py b/tests/toranj/cli/test-015-clear-addresss-cache-for-sed.py
new file mode 100755
index 0000000..31f75cc
--- /dev/null
+++ b/tests/toranj/cli/test-015-clear-addresss-cache-for-sed.py
@@ -0,0 +1,232 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: Address Cache Table
+#
+# This test verifies that address cache entries associated with SED child
+# addresses are removed from new parent node ensuring we would not have a
+# routing loop.
+#
+#
+#   r1 ---- r2 ---- r3
+#                   |
+#                   |
+#                   c
+#
+# c is initially attached to r3 but it switches parent during test to r2 and then r1.
+
+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()
+r3 = cli.Node()
+c = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+
+r3.allowlist_node(r2)
+r3.allowlist_node(c)
+
+c.allowlist_node(r3)
+
+r1.form('sed-cache')
+
+prefix = 'fd00:abba::'
+r1.add_prefix(prefix + '/64', 'paos', 'med')
+r1.register_netdata()
+
+r2.join(r1)
+r3.join(r1)
+c.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+c.set_pollperiod(400)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(r3.get_state() == 'router')
+verify(c.get_state() == 'child')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# Wait till first router has either established a link or
+# has a valid "next hop" towards all other routers.
+
+r1_rloc16 = int(r1.get_rloc16(), 16)
+
+
+def check_r1_router_table():
+    table = r1.get_router_table()
+    verify(len(table) == 3)
+    for entry in table:
+        verify(int(entry['RLOC16'], 0) == r1_rloc16 or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63)
+
+
+verify_within(check_r1_router_table, 120)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+r1_rloc = int(r1.get_rloc16(), 16)
+r2_rloc = int(r2.get_rloc16(), 16)
+r3_rloc = int(r3.get_rloc16(), 16)
+c_rloc = int(c.get_rloc16(), 16)
+
+r1_address = next(addr for addr in r1.get_ip_addrs() if addr.startswith('fd00:abba:'))
+r2_address = next(addr for addr in r2.get_ip_addrs() if addr.startswith('fd00:abba:'))
+r3_address = next(addr for addr in r3.get_ip_addrs() if addr.startswith('fd00:abba:'))
+c_address = next(addr for addr in c.get_ip_addrs() if addr.startswith('fd00:abba:'))
+
+port = 4321
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Send a single UDP message from r2 to c. This adds an address cache
+# entry on r2 for c pointing to r3 (the current parent of c).
+
+r2.udp_open()
+r2.udp_send(c_address, port, 'hi_from_r2_to_c')
+
+
+def check_r2_cache_table():
+    cache_table = r2.get_eidcache()
+    for entry in cache_table:
+        fields = entry.strip().split(' ')
+        if (fields[0] == c_address):
+            verify(int(fields[1], 16) == r3_rloc)
+            verify(fields[2] == 'cache')
+            break
+    else:
+        verify(False)
+
+
+verify_within(check_r2_cache_table, 20)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Force c to switch its parent from r3 to r2
+#
+#   r1 ---- r2 ---- r3
+#            |
+#            |
+#            c
+
+r3.un_allowlist_node(c)
+r2.allowlist_node(c)
+c.allowlist_node(r2)
+c.un_allowlist_node(r3)
+
+c.thread_stop()
+c.thread_start()
+
+
+def check_c_attaches_to_r2():
+    verify(c.get_state() == 'child')
+    verify(int(c.get_parent_info()['Rloc'], 16) == r2_rloc)
+
+
+verify_within(check_c_attaches_to_r2, 10)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Send a single UDP message from r3 to c. This adds an address cache
+# entry on r3 for c pointing to r2 (the current parent of c).
+
+r3.udp_open()
+r3.udp_send(c_address, port, 'hi_from_r3_to_c')
+
+
+def check_r3_cache_table():
+    cache_table = r3.get_eidcache()
+    for entry in cache_table:
+        fields = entry.strip().split(' ')
+        if (fields[0] == c_address):
+            verify(int(fields[1], 16) == r2_rloc)
+            verify(fields[2] == 'cache')
+            break
+    else:
+        verify(False)
+
+
+verify_within(check_r3_cache_table, 20)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Force c to switch its parent again now to r1
+#
+#   r1 ---- r2 ---- r3
+#   |
+#   |
+#   c
+
+r2.un_allowlist_node(c)
+r1.allowlist_node(c)
+c.allowlist_node(r1)
+c.un_allowlist_node(r2)
+
+c.thread_stop()
+c.thread_start()
+
+
+def check_c_attaches_to_r1():
+    verify(c.get_state() == 'child')
+    verify(int(c.get_parent_info()['Rloc'], 16) == r1_rloc)
+
+
+verify_within(check_c_attaches_to_r1, 10)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Now ping c from r2.
+#
+# If r2 address cache entry is not cleared when c attached to r1, r2
+# will still have an entry pointing to r3, and r3 will have an entry
+# pointing to r2, thus creating a loop (the msg will not be delivered
+# to r3)
+
+r2.ping(c_address)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-016-child-mode-change.py b/tests/toranj/cli/test-016-child-mode-change.py
new file mode 100755
index 0000000..b8cfa2e
--- /dev/null
+++ b/tests/toranj/cli/test-016-child-mode-change.py
@@ -0,0 +1,214 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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:
+#
+# Verify device mode change on children.
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+parent = cli.Node()
+child1 = cli.Node()
+child2 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+parent.form('modechange')
+child1.join(parent, cli.JOIN_TYPE_END_DEVICE)
+child2.join(parent, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+
+verify(parent.get_state() == 'leader')
+verify(child1.get_state() == 'child')
+verify(child2.get_state() == 'child')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+child1_rloc = int(child1.get_rloc16(), 16)
+child2_rloc = int(child2.get_rloc16(), 16)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Check the mode on children and also on parent child table
+
+verify(parent.get_mode() == 'rdn')
+verify(child1.get_mode() == 'rn')
+verify(child2.get_mode() == '-')
+
+child_table = parent.get_child_table()
+verify(len(child_table) == 2)
+for entry in child_table:
+    if int(entry['RLOC16'], 16) == child1_rloc:
+        verify(entry['R'] == '1')
+        verify(entry['D'] == '0')
+        verify(entry['N'] == '1')
+    elif int(entry['RLOC16'], 16) == child2_rloc:
+        verify(entry['R'] == '0')
+        verify(entry['D'] == '0')
+        verify(entry['N'] == '0')
+    else:
+        verify(False)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Change the network data flag on child 2 (sleepy) and verify that it
+# gets changed on parent's child table.
+
+child2.set_mode('n')
+
+
+def check_child2_n_flag_change():
+    verify(child2.get_mode() == 'n')
+    child_table = parent.get_child_table()
+    verify(len(child_table) == 2)
+    for entry in child_table:
+        if int(entry['RLOC16'], 16) == child1_rloc:
+            verify(entry['R'] == '1')
+            verify(entry['D'] == '0')
+            verify(entry['N'] == '1')
+        elif int(entry['RLOC16'], 16) == child2_rloc:
+            verify(entry['R'] == '0')
+            verify(entry['D'] == '0')
+            verify(entry['N'] == '1')
+        else:
+            verify(False)
+
+
+verify_within(check_child2_n_flag_change, 5)
+
+# Verify that mode change did not cause child2 to detach and re-attach
+
+verify(int(cli.Node.parse_list(child2.get_mle_counter())['Role Detached']) == 1)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Change child1 from rx-on to sleepy and verify the change on
+# parent's child table. This mode change should require child
+# to detach and attach again.
+
+child1.set_mode('n')
+
+
+def check_child1_become_sleepy():
+    verify(child1.get_mode() == 'n')
+    child_table = parent.get_child_table()
+    verify(len(child_table) == 2)
+    for entry in child_table:
+        if int(entry['RLOC16'], 16) == child1_rloc:
+            verify(entry['R'] == '0')
+            verify(entry['D'] == '0')
+            verify(entry['N'] == '1')
+        elif int(entry['RLOC16'], 16) == child2_rloc:
+            verify(entry['R'] == '0')
+            verify(entry['D'] == '0')
+            verify(entry['N'] == '1')
+        else:
+            verify(False)
+
+
+verify_within(check_child1_become_sleepy, 5)
+
+verify(int(cli.Node.parse_list(child1.get_mle_counter())['Role Detached']) == 2)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Change child2 from sleepy to rx-on and verify the change on
+# parent's child table. Verify that child2 did not detach and
+# used MLE "Child Update" exchange.
+
+child2.set_mode('rn')
+
+
+def check_child2_become_rx_on():
+    verify(child2.get_mode() == 'rn')
+    child_table = parent.get_child_table()
+    verify(len(child_table) == 2)
+    for entry in child_table:
+        if int(entry['RLOC16'], 16) == child1_rloc:
+            verify(entry['R'] == '0')
+            verify(entry['D'] == '0')
+            verify(entry['N'] == '1')
+        elif int(entry['RLOC16'], 16) == child2_rloc:
+            verify(entry['R'] == '1')
+            verify(entry['D'] == '0')
+            verify(entry['N'] == '1')
+        else:
+            verify(False)
+
+
+verify_within(check_child2_become_rx_on, 5)
+
+verify(int(cli.Node.parse_list(child2.get_mle_counter())['Role Detached']) == 1)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Now change child2 to become sleepy again. Since it was originally
+# attached as sleepy it should not detach and attach again and
+# can do this using MlE "Child Update" exchange with parent.
+
+child2.set_mode('n')
+
+
+def check_child2_become_sleepy_again():
+    verify(child2.get_mode() == 'n')
+    child_table = parent.get_child_table()
+    verify(len(child_table) == 2)
+    for entry in child_table:
+        if int(entry['RLOC16'], 16) == child1_rloc:
+            verify(entry['R'] == '0')
+            verify(entry['D'] == '0')
+            verify(entry['N'] == '1')
+        elif int(entry['RLOC16'], 16) == child2_rloc:
+            verify(entry['R'] == '0')
+            verify(entry['D'] == '0')
+            verify(entry['N'] == '1')
+        else:
+            verify(False)
+
+
+verify_within(check_child2_become_sleepy_again, 5)
+
+verify(int(cli.Node.parse_list(child2.get_mle_counter())['Role Detached']) == 1)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-017-network-data-versions.py b/tests/toranj/cli/test-017-network-data-versions.py
new file mode 100755
index 0000000..d90bb13
--- /dev/null
+++ b/tests/toranj/cli/test-017-network-data-versions.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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 Data update and version changes (stable only vs. full version).
+#
+# Network topology
+#
+#      r1 ---- r2 ---- r3
+#              |
+#              |
+#              sed
+#
+#
+# sed is sleepy-end node and also configured to request stable Network Data only
+#
+# Test covers the following steps:
+# - Adding/removing prefixes (stable or temporary) on r1
+# - Verifying that Network Data is updated on all nodes
+# - Ensuring correct update to version and stable version
+#
+# The above steps are repeated over many different situations:
+# - Where the same prefixes are also added by other nodes
+# - Or the same prefixes are added as off-mesh routes by other nodes
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+r3 = cli.Node()
+sed = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+r2.allowlist_node(sed)
+
+r2.allowlist_node(r2)
+
+sed.allowlist_node(r2)
+
+r1.form('netdata')
+r2.join(r1)
+r3.join(r1)
+sed.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+
+sed.set_mode('-')
+sed.set_pollperiod(400)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+r1_rloc = r1.get_rloc16()
+r2_rloc = r2.get_rloc16()
+r3_rloc = r3.get_rloc16()
+
+versions = r1.get_netdata_versions()
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def verify_versions_incremented():
+    global versions
+    new_versions = r1.get_netdata_versions()
+    verify(new_versions[0] == ((versions[0] + 1) % 256))
+    verify(new_versions[1] == ((versions[1] + 1) % 256))
+    versions = new_versions
+
+
+def verify_stabe_version_incremented():
+    global versions
+    new_versions = r1.get_netdata_versions()
+    verify(new_versions[0] == ((versions[0] + 1) % 256))
+    verify(new_versions[1] == versions[1])
+    versions = new_versions
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Add prefix `fd00:1::/64` on r1 as stable and validate
+# that entries are updated on all nodes and versions are changed.
+
+r1.add_prefix('fd00:1::/64', 'os')
+r1.register_netdata()
+
+
+def check_r1_prefix_added_on_all_nodes():
+    for node in [r1, r2, r3]:
+        verify('fd00:1:0:0::/64 os med ' + r1_rloc in node.get_netdata_prefixes())
+    verify('fd00:1:0:0::/64 os med fffe' in sed.get_netdata_prefixes())
+
+
+verify_within(check_r1_prefix_added_on_all_nodes, 2)
+verify_versions_incremented()
+
+# Add prefix `fd00:2::/64` on r2 as temporary and ensure it is seen on
+# all nodes and not seen on sed.
+
+r2.add_prefix('fd00:2::/64', 'po', 'high')
+r2.register_netdata()
+
+
+def check_r2_prefix_added_on_all_nodes():
+    for node in [r1, r2, r3]:
+        verify('fd00:2:0:0::/64 po high ' + r2_rloc in node.get_netdata_prefixes())
+    verify(not 'fd00:2:0:0::/64 po high fffe' in sed.get_netdata_prefixes())
+
+
+verify_within(check_r2_prefix_added_on_all_nodes, 2)
+verify_stabe_version_incremented()
+
+# Remove prefix `fd00:1::/64` from r1.
+
+r1.remove_prefix('fd00:1::/64')
+r1.register_netdata()
+
+
+def check_r1_prefix_removed_on_all_nodes():
+    for node in [r1, r2, r3]:
+        verify(not 'fd00:1:0:0::/64 os med ' + r1_rloc in node.get_netdata_prefixes())
+    verify(not 'fd00:1:0:0::/64 os med fffe' in sed.get_netdata_prefixes())
+
+
+verify_within(check_r1_prefix_removed_on_all_nodes, 2)
+verify_versions_incremented()
+
+# Remove prefix `fd00:2::/64` from r2.
+
+r2.remove_prefix('fd00:2::/64')
+r2.register_netdata()
+
+
+def check_r2_prefix_removed_on_all_nodes():
+    for node in [r1, r2, r3]:
+        verify(not 'fd00:2:0:0::/64 po high ' + r2_rloc in node.get_netdata_prefixes())
+    verify(not 'fd00:2:0:0::/64 po high fffe' in sed.get_netdata_prefixes())
+
+
+verify_within(check_r2_prefix_removed_on_all_nodes, 2)
+verify_stabe_version_incremented()
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Repeat the same checks but now r3 also adds ``fd00:1::/64` with different
+# flags.
+
+r3.add_prefix('fd00:1::/64', 'paos')
+r3.register_netdata()
+
+
+def check_r3_prefix_added_on_all_nodes():
+    for node in [r1, r2, r3]:
+        verify('fd00:1:0:0::/64 paos med ' + r3_rloc in node.get_netdata_prefixes())
+    verify('fd00:1:0:0::/64 paos med fffe' in sed.get_netdata_prefixes())
+
+
+verify_within(check_r3_prefix_added_on_all_nodes, 2)
+verify_versions_incremented()
+
+r1.add_prefix('fd00:1::/64', 'os')
+r1.register_netdata()
+verify_within(check_r1_prefix_added_on_all_nodes, 2)
+verify_versions_incremented()
+
+r2.add_prefix('fd00:2::/64', 'po', 'high')
+r2.register_netdata()
+verify_within(check_r2_prefix_added_on_all_nodes, 2)
+verify_stabe_version_incremented()
+
+r1.remove_prefix('fd00:1::/64')
+r1.register_netdata()
+verify_within(check_r1_prefix_removed_on_all_nodes, 2)
+verify_versions_incremented()
+
+r2.remove_prefix('fd00:2::/64')
+r2.register_netdata()
+verify_within(check_r2_prefix_removed_on_all_nodes, 2)
+verify_stabe_version_incremented()
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Repeat the same checks with r3 also adding ``fd00:2::/64`
+
+r3.add_prefix('fd00:2::/64', 'paos')
+r3.register_netdata()
+
+
+def check_new_r3_prefix_added_on_all_nodes():
+    for node in [r1, r2, r3]:
+        verify('fd00:2:0:0::/64 paos med ' + r3_rloc in node.get_netdata_prefixes())
+    verify('fd00:2:0:0::/64 paos med fffe' in sed.get_netdata_prefixes())
+
+
+verify_within(check_new_r3_prefix_added_on_all_nodes, 2)
+verify_versions_incremented()
+
+r1.add_prefix('fd00:1::/64', 'os')
+r1.register_netdata()
+verify_within(check_r1_prefix_added_on_all_nodes, 2)
+verify_versions_incremented()
+
+r2.add_prefix('fd00:2::/64', 'po', 'high')
+r2.register_netdata()
+verify_within(check_r2_prefix_added_on_all_nodes, 2)
+verify_stabe_version_incremented()
+
+r1.remove_prefix('fd00:1::/64')
+r1.register_netdata()
+verify_within(check_r1_prefix_removed_on_all_nodes, 2)
+verify_versions_incremented()
+
+r2.remove_prefix('fd00:2::/64')
+r2.register_netdata()
+verify_within(check_r2_prefix_removed_on_all_nodes, 2)
+verify_stabe_version_incremented()
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Repeat the same checks with r3 adding the two prefixes as temporary.
+
+r3.remove_prefix('fd00:1::/64')
+r3.remove_prefix('fd00:2::/64')
+r3.add_prefix('fd00:1::/64', 'pao')
+r3.add_prefix('fd00:2::/64', 'pao')
+r3.register_netdata()
+
+
+def check_r3_prefixes_as_temp_added_on_all_nodes():
+    for node in [r1, r2, r3]:
+        prefixes = node.get_netdata_prefixes()
+        verify('fd00:1:0:0::/64 pao med ' + r3_rloc in prefixes)
+        verify('fd00:1:0:0::/64 pao med ' + r3_rloc in prefixes)
+    verify(len(sed.get_netdata_prefixes()) == 0)
+
+
+verify_within(check_r3_prefixes_as_temp_added_on_all_nodes, 2)
+verify_versions_incremented()
+
+r1.add_prefix('fd00:1::/64', 'os')
+r1.register_netdata()
+verify_within(check_r1_prefix_added_on_all_nodes, 2)
+verify_versions_incremented()
+
+r2.add_prefix('fd00:2::/64', 'po', 'high')
+r2.register_netdata()
+verify_within(check_r2_prefix_added_on_all_nodes, 2)
+verify_stabe_version_incremented()
+
+r1.remove_prefix('fd00:1::/64')
+r1.register_netdata()
+verify_within(check_r1_prefix_removed_on_all_nodes, 2)
+verify_versions_incremented()
+
+r2.remove_prefix('fd00:2::/64')
+r2.register_netdata()
+verify_within(check_r2_prefix_removed_on_all_nodes, 2)
+verify_stabe_version_incremented()
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Finally repeat the same checks with r3 adding the two prefixes
+# as off-mesh route prefix.
+
+r3.remove_prefix('fd00:1::/64')
+r3.remove_prefix('fd00:2::/64')
+r3.add_route('fd00:1::/64', 's')
+r3.add_route('fd00:2::/64', '-')
+r3.register_netdata()
+
+
+def check_r3_routes_added_on_all_nodes():
+    for node in [r1, r2, r3]:
+        routes = node.get_netdata_routes()
+        verify('fd00:1:0:0::/64 s med ' + r3_rloc in routes)
+        verify('fd00:2:0:0::/64 med ' + r3_rloc in routes)
+    verify('fd00:1:0:0::/64 s med fffe' in sed.get_netdata_routes())
+    verify(not 'fd00:1:0:0::/64 med fffe' in sed.get_netdata_routes())
+
+
+verify_within(check_r3_routes_added_on_all_nodes, 2)
+verify_versions_incremented()
+
+r1.add_prefix('fd00:1::/64', 'os')
+r1.register_netdata()
+verify_within(check_r1_prefix_added_on_all_nodes, 2)
+verify_versions_incremented()
+
+r2.add_prefix('fd00:2::/64', 'po', 'high')
+r2.register_netdata()
+verify_within(check_r2_prefix_added_on_all_nodes, 2)
+verify_stabe_version_incremented()
+
+r1.remove_prefix('fd00:1::/64')
+r1.register_netdata()
+verify_within(check_r1_prefix_removed_on_all_nodes, 2)
+verify_versions_incremented()
+
+r2.remove_prefix('fd00:2::/64')
+r2.register_netdata()
+verify_within(check_r2_prefix_removed_on_all_nodes, 2)
+verify_stabe_version_incremented()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
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
new file mode 100755
index 0000000..b31b2f8
--- /dev/null
+++ b/tests/toranj/cli/test-018-next-hop-and-path-cost.py
@@ -0,0 +1,291 @@
+#!/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: Next hop and path cost calculation.
+#
+# Network topology
+#
+#     r1 ---- r2...2...r3
+#    / |       \      /  \
+#   /  |        \    /    \
+# fed1 fed2       r4...1...r5 ---- fed3
+#
+# Link r2 --> r3 is configured to be at link quality of 2.
+# Link r5 --> r4 is configured to be at link quality of 1.
+# Link r1 --> fed2 is configured to be at link quality 1.
+# Other links are at link quality 3 (best possible link quality).
+#
+
+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()
+r3 = cli.Node()
+r4 = cli.Node()
+r5 = cli.Node()
+fed1 = cli.Node()
+fed2 = cli.Node()
+fed3 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+r1.allowlist_node(fed1)
+r1.allowlist_node(fed2)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+r2.allowlist_node(r4)
+
+r3.allowlist_node(r2)
+r3.allowlist_node(r4)
+r3.allowlist_node(r5)
+r3.set_macfilter_lqi_to_node(r2, 2)
+
+r4.allowlist_node(r2)
+r4.allowlist_node(r3)
+r4.allowlist_node(r5)
+r4.set_macfilter_lqi_to_node(r5, 1)
+
+r5.allowlist_node(r4)
+r5.allowlist_node(r3)
+r5.allowlist_node(fed3)
+
+fed1.allowlist_node(r1)
+fed2.allowlist_node(r1)
+fed3.allowlist_node(r5)
+fed2.set_macfilter_lqi_to_node(r1, 1)
+
+r1.form('hop-cost')
+r2.join(r1)
+r3.join(r1)
+r4.join(r1)
+r5.join(r1)
+fed1.join(r1, cli.JOIN_TYPE_REED)
+fed2.join(r1, cli.JOIN_TYPE_REED)
+fed3.join(r1, cli.JOIN_TYPE_REED)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+r1_rloc = int(r1.get_rloc16(), 16)
+r2_rloc = int(r2.get_rloc16(), 16)
+r3_rloc = int(r3.get_rloc16(), 16)
+r4_rloc = int(r4.get_rloc16(), 16)
+r5_rloc = int(r5.get_rloc16(), 16)
+
+fed1_rloc = int(fed1.get_rloc16(), 16)
+fed2_rloc = int(fed2.get_rloc16(), 16)
+fed3_rloc = int(fed3.get_rloc16(), 16)
+
+
+def parse_nexthop(line):
+    # Exmaple: "0x5000 cost:3" -> (0x5000, 3).
+    items = line.strip().split(' ', 2)
+    return (int(items[0], 16), int(items[1].split(':')[1]))
+
+
+def check_nexthops_and_costs():
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # `r1` next hops and costs
+    verify(parse_nexthop(r1.get_nexthop(r1_rloc)) == (r1_rloc, 0))
+    verify(parse_nexthop(r1.get_nexthop(r2_rloc)) == (r2_rloc, 1))
+    verify(parse_nexthop(r1.get_nexthop(r3_rloc)) == (r2_rloc, 3))
+    verify(parse_nexthop(r1.get_nexthop(r4_rloc)) == (r2_rloc, 2))
+    verify(parse_nexthop(r1.get_nexthop(r5_rloc)) == (r2_rloc, 4))
+    verify(parse_nexthop(r1.get_nexthop(fed3_rloc)) == (r2_rloc, 5))
+    # On `r1` its children can be reached directly.
+    verify(parse_nexthop(r1.get_nexthop(fed1_rloc)) == (fed1_rloc, 1))
+    verify(parse_nexthop(r1.get_nexthop(fed2_rloc)) == (fed2_rloc, 1))
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # `r2` next hops and costs
+    verify(parse_nexthop(r2.get_nexthop(r1_rloc)) == (r1_rloc, 1))
+    verify(parse_nexthop(r2.get_nexthop(r2_rloc)) == (r2_rloc, 0))
+    # On `r2` the direct link to `r3` and the path through `r4` both
+    # have the same cost, but the direct link should be preferred.
+    verify(parse_nexthop(r2.get_nexthop(r3_rloc)) == (r3_rloc, 2))
+    verify(parse_nexthop(r2.get_nexthop(r4_rloc)) == (r4_rloc, 1))
+    # On 'r2' the path to `r5` can go through `r3` or `r4`
+    # as both have the same cost.
+    (nexthop, cost) = parse_nexthop(r2.get_nexthop(r5_rloc))
+    verify(cost == 3)
+    verify(nexthop in [r3_rloc, r4_rloc])
+    verify(parse_nexthop(r2.get_nexthop(fed1_rloc)) == (r1_rloc, 2))
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # `r3` next hops and costs
+    verify(parse_nexthop(r3.get_nexthop(r3_rloc)) == (r3_rloc, 0))
+    verify(parse_nexthop(r3.get_nexthop(r5_rloc)) == (r5_rloc, 1))
+    verify(parse_nexthop(r3.get_nexthop(r4_rloc)) == (r4_rloc, 1))
+    verify(parse_nexthop(r3.get_nexthop(r2_rloc)) == (r2_rloc, 2))
+    # On `r3` the path to `r1` can go through `r2` or `r4`
+    # as both have the same cost.
+    (nexthop, cost) = parse_nexthop(r3.get_nexthop(r1_rloc))
+    verify(cost == 3)
+    verify(nexthop in [r2_rloc, r4_rloc])
+    # On `r3` the path to fed1 should use the same next hop as `r1`
+    verify(parse_nexthop(r3.get_nexthop(fed2_rloc)) == (nexthop, 4))
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # `r4` next hops and costs
+    verify(parse_nexthop(r4.get_nexthop(fed1_rloc)) == (r2_rloc, 3))
+    verify(parse_nexthop(r4.get_nexthop(r1_rloc)) == (r2_rloc, 2))
+    verify(parse_nexthop(r4.get_nexthop(r2_rloc)) == (r2_rloc, 1))
+    verify(parse_nexthop(r4.get_nexthop(r3_rloc)) == (r3_rloc, 1))
+    verify(parse_nexthop(r4.get_nexthop(r4_rloc)) == (r4_rloc, 0))
+    # On `r4` even though we have a direct link to `r5`
+    # the path cost through `r3` has a smaller cost over
+    # the direct link cost.
+    verify(parse_nexthop(r4.get_nexthop(r5_rloc)) == (r3_rloc, 2))
+    verify(parse_nexthop(r4.get_nexthop(fed3_rloc)) == (r3_rloc, 3))
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # `r5` next hops and costs
+    verify(parse_nexthop(r5.get_nexthop(fed3_rloc)) == (fed3_rloc, 1))
+    verify(parse_nexthop(r5.get_nexthop(r5_rloc)) == (r5_rloc, 0))
+    verify(parse_nexthop(r5.get_nexthop(r3_rloc)) == (r3_rloc, 1))
+    verify(parse_nexthop(r5.get_nexthop(r4_rloc)) == (r3_rloc, 2))
+    verify(parse_nexthop(r5.get_nexthop(r2_rloc)) == (r3_rloc, 3))
+    verify(parse_nexthop(r5.get_nexthop(r1_rloc)) == (r3_rloc, 4))
+    verify(parse_nexthop(r5.get_nexthop(fed1_rloc)) == (r3_rloc, 5))
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # `fed1` next hops and costs
+    verify(parse_nexthop(fed1.get_nexthop(fed1_rloc)) == (fed1_rloc, 0))
+    verify(parse_nexthop(fed1.get_nexthop(r1_rloc)) == (r1_rloc, 1))
+    verify(parse_nexthop(fed1.get_nexthop(r2_rloc)) == (r1_rloc, 2))
+    verify(parse_nexthop(fed1.get_nexthop(r3_rloc)) == (r1_rloc, 4))
+    verify(parse_nexthop(fed1.get_nexthop(r4_rloc)) == (r1_rloc, 3))
+    verify(parse_nexthop(fed1.get_nexthop(r5_rloc)) == (r1_rloc, 5))
+    verify(parse_nexthop(fed1.get_nexthop(fed3_rloc)) == (r1_rloc, 6))
+    # On `fed1`, path to `fed2` should go through our parent.
+    verify(parse_nexthop(fed1.get_nexthop(fed2_rloc)) == (r1_rloc, 2))
+
+    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # `fed2` next hops and costs
+    verify(parse_nexthop(fed2.get_nexthop(fed2_rloc)) == (fed2_rloc, 0))
+    verify(parse_nexthop(fed2.get_nexthop(r1_rloc)) == (r1_rloc, 4))
+    verify(parse_nexthop(fed2.get_nexthop(r2_rloc)) == (r1_rloc, 5))
+    verify(parse_nexthop(fed2.get_nexthop(r3_rloc)) == (r1_rloc, 7))
+    verify(parse_nexthop(fed2.get_nexthop(r4_rloc)) == (r1_rloc, 6))
+    verify(parse_nexthop(fed2.get_nexthop(r5_rloc)) == (r1_rloc, 8))
+    verify(parse_nexthop(fed2.get_nexthop(fed3_rloc)) == (r1_rloc, 9))
+    verify(parse_nexthop(fed2.get_nexthop(fed1_rloc)) == (r1_rloc, 5))
+
+
+verify_within(check_nexthops_and_costs, 5)
+
+#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Disable `r4` and check nexthop and cost on it and on other
+# nodes.
+#
+#
+#     r1 ---- r2...2...r3
+#    / |                 \
+#   /  |                  \
+# fed1 fed2       r4       r5 ---- fed3
+
+r4.thread_stop()
+r4.interface_down()
+
+verify(parse_nexthop(r4.get_nexthop(r2_rloc)) == (0xfffe, 16))
+verify(parse_nexthop(r4.get_nexthop(r4_rloc)) == (0xfffe, 16))
+
+
+def check_nexthops_and_costs_after_r4_detach():
+    # Make sure we have no next hop towards `r4`.
+    verify(parse_nexthop(r1.get_nexthop(r4_rloc)) == (0xfffe, 16))
+    verify(parse_nexthop(r2.get_nexthop(r4_rloc)) == (0xfffe, 16))
+    verify(parse_nexthop(r3.get_nexthop(r4_rloc)) == (0xfffe, 16))
+    verify(parse_nexthop(r5.get_nexthop(r4_rloc)) == (0xfffe, 16))
+    verify(parse_nexthop(fed3.get_nexthop(r4_rloc)) == (r5_rloc, 16))
+    # Check cost and next hop on other nodes
+    verify(parse_nexthop(r1.get_nexthop(r5_rloc)) == (r2_rloc, 4))
+    verify(parse_nexthop(r2.get_nexthop(r3_rloc)) == (r3_rloc, 2))
+    verify(parse_nexthop(r3.get_nexthop(r2_rloc)) == (r2_rloc, 2))
+    verify(parse_nexthop(fed3.get_nexthop(fed1_rloc)) == (r5_rloc, 6))
+
+
+verify_within(check_nexthops_and_costs_after_r4_detach, 45)
+verify(r1.get_state() == 'leader')
+
+#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Disable `r1` (which was previous leader) and check
+# routes on other nodes
+#
+#
+#     r1      r2...2...r3
+#    / |                 \
+#   /  |                  \
+# fed1 fed2       r4       r5 ---- fed3
+
+r1.thread_stop()
+r1.interface_down()
+fed1.thread_stop()
+fed1.interface_down()
+fed2.thread_stop()
+fed2.interface_down()
+
+verify(parse_nexthop(r1.get_nexthop(r2_rloc)) == (0xfffe, 16))
+verify(parse_nexthop(r1.get_nexthop(r1_rloc)) == (0xfffe, 16))
+verify(parse_nexthop(r1.get_nexthop(fed1_rloc)) == (0xfffe, 16))
+
+
+def check_nexthops_and_costs_after_r1_detach():
+    verify(parse_nexthop(r2.get_nexthop(r1_rloc)) == (0xfffe, 16))
+    verify(parse_nexthop(r3.get_nexthop(r1_rloc)) == (0xfffe, 16))
+    verify(parse_nexthop(r5.get_nexthop(r1_rloc)) == (0xfffe, 16))
+    verify(parse_nexthop(fed3.get_nexthop(r1_rloc)) == (r5_rloc, 16))
+    verify(parse_nexthop(r2.get_nexthop(r5_rloc)) == (r3_rloc, 3))
+    verify(parse_nexthop(fed3.get_nexthop(r2_rloc)) == (r5_rloc, 4))
+
+
+verify_within(check_nexthops_and_costs_after_r1_detach, 30)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-019-netdata-context-id.py b/tests/toranj/cli/test-019-netdata-context-id.py
new file mode 100755
index 0000000..68029da
--- /dev/null
+++ b/tests/toranj/cli/test-019-netdata-context-id.py
@@ -0,0 +1,373 @@
+#!/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 Data Context ID assignment and reuse delay.
+#
+# Network topology
+#
+#      r1 ---- r2 ---- r3
+#
+
+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()
+r3 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+
+r3.allowlist_node(r2)
+
+r1.form('netdata')
+r2.join(r1)
+r3.join(r1)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(r3.get_state() == 'router')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# Check the default reuse delay on `r1` to be 5 minutes.
+
+verify(int(r1.get_context_reuse_delay()) == 5 * 60)
+
+# Change the reuse delay on `r1` (leader) to 5 seconds.
+
+r1.set_context_reuse_delay(5)
+verify(int(r1.get_context_reuse_delay()) == 5)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Add an on-mesh prefix from each router `r1`, r2`, and `r3`.
+# Validate that netdata is updated and Context IDs are assigned.
+
+r1.add_prefix('fd00:1::/64', 'poas')
+r1.register_netdata()
+
+r2.add_prefix('fd00:2::/64', 'poas')
+r2.register_netdata()
+
+r3.add_prefix('fd00:3::/64', 'poas')
+r3.add_route('fd00:beef::/64', 's')
+r3.register_netdata()
+
+
+def check_netdata_1():
+    global netdata
+    netdata = r1.get_netdata()
+    verify(len(netdata['prefixes']) == 3)
+    verify(len(netdata['routes']) == 1)
+
+
+verify_within(check_netdata_1, 5)
+
+contexts = netdata['contexts']
+verify(len(contexts) == 3)
+verify(any([context.startswith('fd00:1:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:2:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:3:0:0::/64') for context in contexts]))
+for context in contexts:
+    verify(context.endswith('c'))
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Remove prefix on `r3`. Validate that Context compress flag
+# is cleared for the removed prefix.
+
+r3.remove_prefix('fd00:3::/64')
+r3.register_netdata()
+
+
+def check_netdata_2():
+    global netdata, versions
+    versions = r1.get_netdata_versions()
+    netdata = r1.get_netdata()
+    verify(len(netdata['prefixes']) == 2)
+    verify(len(netdata['routes']) == 1)
+
+
+verify_within(check_netdata_2, 5)
+
+contexts = netdata['contexts']
+verify(len(contexts) == 3)
+verify(any([context.startswith('fd00:1:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:2:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:3:0:0::/64') for context in contexts]))
+for context in contexts:
+    if context.startswith('fd00:1:0:0::/64') or context.startswith('fd00:2:0:0::/64'):
+        verify(context.endswith('c'))
+    else:
+        verify(context.endswith('-'))
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Validate that the prefix context is removed within reuse delay
+# time (which is set to 5 seconds on leader).
+
+
+def check_netdata_3():
+    global netdata
+    netdata = r1.get_netdata()
+    verify(len(netdata['prefixes']) == 2)
+    verify(len(netdata['routes']) == 1)
+    verify(len(netdata['contexts']) == 2)
+
+
+verify_within(check_netdata_3, 5)
+old_versions = versions
+versions = r1.get_netdata_versions()
+verify(versions != old_versions)
+
+# Make sure netdata does not change afterwards
+
+time.sleep(5 * 3 / speedup)
+
+verify(versions == r1.get_netdata_versions())
+netdata = r1.get_netdata()
+verify(len(netdata['prefixes']) == 2)
+verify(len(netdata['routes']) == 1)
+verify(len(netdata['contexts']) == 2)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Add two new on-mesh prefixes from `r3`. Validate that they get
+# assigned Context IDs.
+
+r3.add_prefix('fd00:4::/64', 'poas')
+r3.register_netdata()
+
+r3.add_prefix('fd00:3::/64', 'poas')
+r3.register_netdata()
+
+
+def check_netdata_4():
+    global netdata
+    netdata = r1.get_netdata()
+    verify(len(netdata['prefixes']) == 4)
+    verify(len(netdata['routes']) == 1)
+    verify(len(netdata['contexts']) == 4)
+
+
+verify_within(check_netdata_4, 5)
+
+contexts = netdata['contexts']
+verify(len(contexts) == 4)
+verify(any([context.startswith('fd00:1:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:2:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:3:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:4:0:0::/64') for context in contexts]))
+for context in contexts:
+    verify(context.endswith('c'))
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Remove prefixes on `r1` and `r2` and re-add them back quickly both
+# from `r2`. Validate that they get the same context IDs as before.
+
+for context in contexts:
+    if context.startswith('fd00:1:0:0::/64'):
+        cid1 = int(context.split()[1])
+    elif context.startswith('fd00:2:0:0::/64'):
+        cid2 = int(context.split()[1])
+
+r1.remove_prefix('fd00:1:0:0::/64')
+r1.register_netdata()
+
+r2.remove_prefix('fd00:2:0:0::/64')
+r2.register_netdata()
+
+
+def check_netdata_5():
+    global netdata
+    netdata = r1.get_netdata()
+    verify(len(netdata['prefixes']) == 2)
+    verify(len(netdata['routes']) == 1)
+
+
+verify_within(check_netdata_5, 5)
+
+contexts = netdata['contexts']
+verify(len(contexts) == 4)
+verify(any([context.startswith('fd00:1:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:2:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:3:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:4:0:0::/64') for context in contexts]))
+for context in contexts:
+    if context.startswith('fd00:1:0:0::/64') or context.startswith('fd00:2:0:0::/64'):
+        verify(context.endswith('-'))
+    else:
+        verify(context.endswith('c'))
+
+# Re-add both prefixes (now from `r2`) before CID remove delay time
+# is expired.
+
+r2.add_prefix('fd00:1::/64', 'poas')
+r2.add_prefix('fd00:2::/64', 'poas')
+r2.register_netdata()
+
+
+def check_netdata_6():
+    global netdata
+    netdata = r1.get_netdata()
+    verify(len(netdata['prefixes']) == 4)
+    verify(len(netdata['routes']) == 1)
+
+
+verify_within(check_netdata_6, 5)
+
+contexts = netdata['contexts']
+verify(len(contexts) == 4)
+verify(any([context.startswith('fd00:1:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:2:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:3:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:4:0:0::/64') for context in contexts]))
+for context in contexts:
+    verify(context.endswith('c'))
+    if context.startswith('fd00:1:0:0::/64'):
+        verify(int(context.split()[1]) == cid1)
+    elif context.startswith('fd00:2:0:0::/64'):
+        verify(int(context.split()[1]) == cid2)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Remove two prefixes on `r3`. Add one back as an external route from
+# `r1`. Also add a new prefix from `r1`. Validate that the context IDs
+# for the removed on-mesh prefixes are removed after reuse delay time
+# and that the new prefix gets a different Context ID.
+
+# Remember the CID used.
+for context in contexts:
+    if context.startswith('fd00:3:0:0::/64'):
+        cid3 = int(context.split()[1])
+    elif context.startswith('fd00:4:0:0::/64'):
+        cid4 = int(context.split()[1])
+
+r3.remove_prefix('fd00:3:0:0::/64')
+r3.remove_prefix('fd00:4:0:0::/64')
+r3.register_netdata()
+
+
+def check_netdata_7():
+    global netdata
+    netdata = r1.get_netdata()
+    verify(len(netdata['prefixes']) == 2)
+    verify(len(netdata['routes']) == 1)
+
+
+verify_within(check_netdata_7, 5)
+
+contexts = netdata['contexts']
+verify(len(contexts) == 4)
+verify(any([context.startswith('fd00:1:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:2:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:3:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:4:0:0::/64') for context in contexts]))
+for context in contexts:
+    if context.startswith('fd00:3:0:0::/64') or context.startswith('fd00:4:0:0::/64'):
+        verify(context.endswith('-'))
+    else:
+        verify(context.endswith('c'))
+
+# Add first one removed as route and add a new prefix.
+
+r1.add_route('fd00:3:0:0::/64', 's')
+r1.add_prefix('fd00:5:0:0::/64', 'poas')
+r1.register_netdata()
+
+
+def check_netdata_8():
+    global netdata
+    netdata = r1.get_netdata()
+    verify(len(netdata['prefixes']) == 3)
+    verify(len(netdata['routes']) == 2)
+    verify(len(netdata['contexts']) == 3)
+
+
+verify_within(check_netdata_8, 5)
+
+contexts = netdata['contexts']
+verify(len(contexts) == 3)
+verify(any([context.startswith('fd00:1:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:2:0:0::/64') for context in contexts]))
+verify(any([context.startswith('fd00:5:0:0::/64') for context in contexts]))
+
+for context in contexts:
+    verify(context.endswith('c'))
+    if context.startswith('fd00:5:0:0::/64'):
+        verify(not int(context.split()[1]) in [cid3, cid4])
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Remove a prefix on `r2`, wait one second and remove a second
+# prefix. Validate the Context IDs for both are removed.
+
+r2.remove_prefix('fd00:1::/64')
+r2.register_netdata()
+
+time.sleep(1 / speedup)
+
+r2.remove_prefix('fd00:2::/64')
+r2.register_netdata()
+
+
+def check_netdata_9():
+    global netdata
+    netdata = r1.get_netdata()
+    verify(len(netdata['prefixes']) == 1)
+    verify(len(netdata['routes']) == 2)
+    verify(len(netdata['contexts']) == 1)
+
+
+verify_within(check_netdata_9, 5)
+
+contexts = netdata['contexts']
+verify(len(contexts) == 1)
+verify(contexts[0].startswith('fd00:5:0:0::/64'))
+verify(contexts[0].endswith('c'))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
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
new file mode 100755
index 0000000..cb67d64
--- /dev/null
+++ b/tests/toranj/cli/test-601-channel-manager-channel-change.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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:
+#
+# Verifies `ChannelManager` channel change process.
+#
+# Network Topology:
+#
+#    r1  --- r2 --- r3
+#    /\      |      |
+#   /  \     |      |
+#  sc1 ec1  sc2     sc3
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 20
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+r3 = cli.Node()
+sc1 = cli.Node()
+ec1 = cli.Node()
+sc2 = cli.Node()
+sc3 = cli.Node()
+
+nodes = [r1, r2, r3, sc1, ec1, sc2, sc3]
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.allowlist_node(r2)
+r1.allowlist_node(sc1)
+r1.allowlist_node(ec1)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+r2.allowlist_node(sc2)
+
+r3.allowlist_node(r2)
+r3.allowlist_node(sc3)
+
+sc1.allowlist_node(r1)
+ec1.allowlist_node(r1)
+sc2.allowlist_node(r2)
+sc3.allowlist_node(r3)
+
+r1.form('chan-man', channel=11)
+r2.join(r1)
+r3.join(r1)
+sc1.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+ec1.join(r1, cli.JOIN_TYPE_END_DEVICE)
+sc2.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+sc3.join(r1, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+
+sc1.set_pollperiod(500)
+sc2.set_pollperiod(500)
+sc3.set_pollperiod(500)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(r3.get_state() == 'router')
+verify(ec1.get_state() == 'child')
+verify(sc1.get_state() == 'child')
+verify(sc2.get_state() == 'child')
+verify(sc3.get_state() == 'child')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+result = cli.Node.parse_list(r1.cli('channel manager'))
+verify(result['channel'] == '0')
+verify(result['auto'] == '0')
+
+r1.cli('channel manager delay 4')
+verify(int(r1.cli('channel manager delay')[0]) == 4)
+
+channel = 11
+
+
+def check_channel_on_all_nodes():
+    verify(all(int(node.get_channel()) == channel for node in nodes))
+
+
+verify_within(check_channel_on_all_nodes, 10)
+
+# Request a channel change to channel 13 from router r1. Verify
+# that all nodes switch to the new channel.
+
+channel = 13
+r1.cli('channel manager change', channel)
+verify_within(check_channel_on_all_nodes, 10)
+
+# Request same channel change on multiple routers at the same time.
+
+channel = 14
+r1.cli('channel manager change', channel)
+r2.cli('channel manager change', channel)
+r3.cli('channel manager change', channel)
+verify_within(check_channel_on_all_nodes, 10)
+
+# Request different channel changes from same router back-to-back.
+
+channel = 15
+r1.cli('channel manager change', channel)
+time.sleep(1 / speedup)
+channel = 16
+r1.cli('channel manager change', channel)
+verify_within(check_channel_on_all_nodes, 10)
+
+# Request different channels from two routers (r1 and r2).
+# We increase delay on r1 to make sure r1 is in middle of
+# channel when new change is requested from r2.
+
+r1.cli('channel manager delay 20')
+r1.cli('channel manager change 17')
+time.sleep(5 / speedup)
+verify_within(check_channel_on_all_nodes, 10)
+channael = 18
+r2.cli('channel manager change', channel)
+verify_within(check_channel_on_all_nodes, 10)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-602-channel-manager-channel-select.py b/tests/toranj/cli/test-602-channel-manager-channel-select.py
new file mode 100755
index 0000000..0ddf358
--- /dev/null
+++ b/tests/toranj/cli/test-602-channel-manager-channel-select.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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:
+#
+# Verifies `ChannelManager` channel selection procedure
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+# Run the test with 10,000 time speedup factor
+speedup = 10000
+cli.Node.set_time_speedup_factor(speedup)
+
+node = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+node.form('chan-sel', channel=24)
+
+verify(node.get_state() == 'leader')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+channel = 24
+
+
+def check_channel():
+    verify(int(node.get_channel()) == channel)
+
+
+check_channel()
+
+all_channels_mask = int('0x7fff800', 0)
+chan_12_to_15_mask = int('0x000f000', 0)
+chan_15_to_17_mask = int('0x0038000', 0)
+
+# Set supported channel mask to be all channels
+node.cli('channel manager supported', all_channels_mask)
+
+# Sleep for 4.5 second with speedup factor of 10,000 this is more than 12
+# hours. We sleep instead of immediately checking the sample counter in
+# order to not add more actions/events into simulation (specially since
+# we are running at very high speedup).
+time.sleep(4.5)
+
+result = cli.Node.parse_list(node.cli('channel monitor')[:5])
+verify(result['enabled'] == '1')
+verify(int(result['count']) > 970)
+
+# Issue a channel-select with quality check enabled, and verify that no
+# action is taken.
+
+node.cli('channel manager select 0')
+result = cli.Node.parse_list(node.cli('channel manager'))
+verify(result['channel'] == '0')
+
+# Issue a channel-select with quality check disabled, verify that channel
+# is switched to channel 11.
+
+node.cli('channel manager select 1')
+result = cli.Node.parse_list(node.cli('channel manager'))
+verify(result['channel'] == '11')
+channel = 11
+verify_within(check_channel, 2)
+
+# Set channels 12-15 as favorable and request a channel select, verify
+# that channel is switched to 12.
+#
+# Even though 11 would be best, quality difference between 11 and 12
+# is not high enough for selection algorithm to pick an unfavored
+# channel.
+
+node.cli('channel manager favored', chan_12_to_15_mask)
+
+channel = 25
+node.cli('channel manager change', channel)
+verify_within(check_channel, 2)
+
+node.cli('channel manager select 1')
+result = cli.Node.parse_list(node.cli('channel manager'))
+verify(result['channel'] == '12')
+channel = 12
+verify_within(check_channel, 2)
+
+# Set channels 15-17 as favorables and request a channel select,
+# verify that channel is switched to 11.
+#
+# This time the quality difference between 11 and 15 should be high
+# enough for selection algorithm to pick the best though unfavored
+# channel (i.e., channel 11).
+
+channel = 25
+node.cli('channel manager change', channel)
+verify_within(check_channel, 2)
+
+node.cli('channel manager favored', chan_15_to_17_mask)
+
+node.cli('channel manager select 1')
+result = cli.Node.parse_list(node.cli('channel manager'))
+verify(result['channel'] == '11')
+channel = 11
+verify_within(check_channel, 2)
+
+# Set channels 12-15 as favorable and request a channel select, verify
+# that channel is not switched.
+
+node.cli('channel manager favored', chan_12_to_15_mask)
+
+node.cli('channel manager select 1')
+result = cli.Node.parse_list(node.cli('channel manager'))
+verify(result['channel'] == '11')
+channel = 11
+verify_within(check_channel, 2)
+
+# Starting from channel 12 and issuing a channel select (which would
+# pick 11 as best channel). However, since quality difference between
+# current channel 12 and new best channel 11 is not large enough, no
+# action should be taken.
+
+channel = 12
+node.cli('channel manager change', channel)
+verify_within(check_channel, 2)
+
+node.cli('channel manager favored', all_channels_mask)
+
+node.cli('channel manager select 1')
+result = cli.Node.parse_list(node.cli('channel manager'))
+verify(result['channel'] == '12')
+verify_within(check_channel, 2)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-603-channel-announce-recovery.py b/tests/toranj/cli/test-603-channel-announce-recovery.py
new file mode 100755
index 0000000..6482aa9
--- /dev/null
+++ b/tests/toranj/cli/test-603-channel-announce-recovery.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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:
+#
+# Orphaned node attach through MLE Announcement after channel change.
+
+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)
+
+router = cli.Node()
+c1 = cli.Node()
+c2 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+router.form('announce-tst', channel=11)
+
+c1.join(router, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+c2.join(router, cli.JOIN_TYPE_SLEEPY_END_DEVICE)
+c1.set_pollperiod(500)
+c2.set_pollperiod(500)
+
+verify(router.get_state() == 'leader')
+verify(c1.get_state() == 'child')
+verify(c2.get_state() == 'child')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# Stop c2
+
+c2.thread_stop()
+
+# Switch the rest of network to channel 26
+router.cli('channel manager change 26')
+
+
+def check_channel_changed_to_26_on_r1_c1():
+    for node in [router, c1]:
+        verify(int(node.get_channel()) == 26)
+
+
+verify_within(check_channel_changed_to_26_on_r1_c1, 10)
+
+# Now re-enable c2 and verify that it does attach to router and is on
+# channel 26. c2 would go through the ML Announce recovery.
+
+c2.thread_start()
+verify(int(c2.get_channel()) == 11)
+
+# wait for 20s for c2 to be attached/associated
+
+
+def check_c2_is_attched():
+    verify(c2.get_state() == 'child')
+
+
+verify_within(check_c2_is_attched, 20)
+
+# Check that c2 is now on channel 26.
+verify(int(c2.get_channel()) == 26)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-700-multi-radio-join.py b/tests/toranj/cli/test-700-multi-radio-join.py
new file mode 100755
index 0000000..236a0f0
--- /dev/null
+++ b/tests/toranj/cli/test-700-multi-radio-join.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: This test covers joining of nodes with different radio links
+#
+# Parent node with trel and 15.4 with 3 children.
+#
+#        parent
+#    (trel + 15.4)
+#      /    |    \
+#     /     |     \
+#    /      |      \
+#   c1     c2       c3
+# (15.4)  (trel)  (trel+15.4)
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+parent = cli.Node(cli.RADIO_15_4_TREL)
+c1 = cli.Node(cli.RADIO_15_4)
+c2 = cli.Node(cli.RADIO_TREL)
+c3 = cli.Node(cli.RADIO_15_4_TREL)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+# Verify that each node supports the correct radio links.
+
+verify(parent.multiradio_get_radios() == '[15.4, TREL]')
+verify(c1.multiradio_get_radios() == '[15.4]')
+verify(c2.multiradio_get_radios() == '[TREL]')
+verify(c3.multiradio_get_radios() == '[15.4, TREL]')
+
+parent.form("multi-radio")
+
+c1.join(parent, cli.JOIN_TYPE_END_DEVICE)
+c2.join(parent, cli.JOIN_TYPE_END_DEVICE)
+c3.join(parent, cli.JOIN_TYPE_END_DEVICE)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Verify that parent correctly learns about all the children and their
+# supported radio links.
+
+c1_ext_addr = c1.get_ext_addr()
+c2_ext_addr = c2.get_ext_addr()
+c3_ext_addr = c3.get_ext_addr()
+
+neighbor_radios = parent.multiradio_get_neighbor_list()
+verify(len(neighbor_radios) == 3)
+for entry in neighbor_radios:
+    info = cli.Node.parse_multiradio_neighbor_entry(entry)
+    radios = info['Radios']
+    if info['ExtAddr'] == c1_ext_addr:
+        verify(int(info['RLOC16'], 16) == int(c1.get_rloc16(), 16))
+        verify(len(radios) == 1)
+        verify('15.4' in radios)
+    elif info['ExtAddr'] == c2_ext_addr:
+        verify(int(info['RLOC16'], 16) == int(c2.get_rloc16(), 16))
+        verify(len(radios) == 1)
+        verify('TREL' in radios)
+    elif info['ExtAddr'] == c3_ext_addr:
+        verify(int(info['RLOC16'], 16) == int(c3.get_rloc16(), 16))
+        verify(len(radios) == 2)
+        verify('15.4' in radios)
+        verify('TREL' in radios)
+    else:
+        verify(False)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Reset the parent and check that all children are attached back successfully
+
+del parent
+parent = cli.Node(cli.RADIO_15_4_TREL, index=1)
+parent.interface_up()
+parent.thread_start()
+
+
+def check_all_children_are_attached():
+    verify(len(parent.get_child_table()) == 3)
+
+
+verify_within(check_all_children_are_attached, 10)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-701-multi-radio-probe.py b/tests/toranj/cli/test-701-multi-radio-probe.py
new file mode 100755
index 0000000..bc0f128
--- /dev/null
+++ b/tests/toranj/cli/test-701-multi-radio-probe.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: This test covers behavior of device after trel network is temporarily disabled
+# and rediscovery of trel radio using probe mechanism.
+#
+#   r1  ---------- r2
+# (15.4+trel)   (15.4+trel)
+#
+#  On r2 we disable trel temporarily.
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node(cli.RADIO_15_4_TREL)
+r2 = cli.Node(cli.RADIO_15_4_TREL)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Build network topology
+
+r1.form("prove-discover")
+r2.join(r1)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+high_preference_threshold = 220
+min_preference_threshold = 0
+
+verify(r1.multiradio_get_radios() == '[15.4, TREL]')
+verify(r2.multiradio_get_radios() == '[15.4, TREL]')
+
+r1_rloc = int(r1.get_rloc16(), 16)
+r2_rloc = int(r2.get_rloc16(), 16)
+
+r1_ml_addr = r1.get_mleid_ip_addr()
+r2_ml_addr = r2.get_mleid_ip_addr()
+
+# Wait till routes are discovered.
+
+
+def check_r1_router_table():
+    table = r1.get_router_table()
+    verify(len(table) == 2)
+    for entry in table:
+        verify(int(entry['RLOC16'], 0) == r1_rloc or int(entry['Link']) == 1)
+
+
+verify_within(check_r1_router_table, 120)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Verify that r1 detected both TREL & 15.4 as supported radios by r2
+
+
+def check_r1_sees_r2_has_two_radio_links():
+    neighbor_radios = r1.multiradio_get_neighbor_list()
+    verify(len(neighbor_radios) == 1)
+    info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0])
+    verify(int(info['RLOC16'], 16) == r2_rloc)
+    radios = info['Radios']
+    verify(len(radios) == 2)
+    verify('15.4' in radios)
+    verify('TREL' in radios)
+
+
+cli.verify_within(check_r1_sees_r2_has_two_radio_links, 10)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Ping r2 from r1 and verify that r1 prefers trel radio link for
+# sending to r2.
+
+r1.ping(r2_ml_addr, count=5)
+
+neighbor_radios = r1.multiradio_get_neighbor_list()
+verify(len(neighbor_radios) == 1)
+info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0])
+radios = info['Radios']
+verify(radios['TREL'] >= high_preference_threshold)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Now temporary filter trel link on r2 and ping again. We expect that
+# r1 to quickly detect that trel is no longer supported by r2 and
+# prefer 15.4 for tx to r2.
+
+r2.cli('trel filter enable')
+verify(r2.cli('trel filter')[0] == 'Enabled')
+
+r1.udp_open()
+for count in range(5):
+    r1.udp_send(r2_ml_addr, 12345, 'hi_r2_from_r1')
+
+
+def check_r1_does_not_prefer_trel_for_r2():
+    neighbor_radios = r1.multiradio_get_neighbor_list()
+    verify(len(neighbor_radios) == 1)
+    info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0])
+    radios = info['Radios']
+    verify(radios['TREL'] <= min_preference_threshold)
+
+
+verify_within(check_r1_does_not_prefer_trel_for_r2, 10)
+
+# Check that we can send between r1 and r2 (now all tx should use 15.4)
+
+r1.ping(r2_ml_addr, count=5, verify_success=False)
+r1.ping(r2_ml_addr, count=5)
+
+neighbor_radios = r1.multiradio_get_neighbor_list()
+verify(len(neighbor_radios) == 1)
+info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0])
+radios = info['Radios']
+verify(radios['TREL'] <= min_preference_threshold)
+verify(radios['15.4'] >= high_preference_threshold)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Enable trel back on r2, start sending traffic from r1 to r2
+# The probe mechanism should kick and detect that r2 has trel again.
+
+r2.cli('trel filter disable')
+verify(r2.cli('trel filter')[0] == 'Disabled')
+
+r2.udp_open()
+for count in range(80):
+    r2.udp_send(r1_ml_addr, 12345, 'hi_r1_from_r2')
+
+
+def check_r1_again_prefers_trel_for_r2():
+    neighbor_radios = r1.multiradio_get_neighbor_list()
+    verify(len(neighbor_radios) == 1)
+    info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0])
+    radios = info['Radios']
+    verify(radios['TREL'] >= high_preference_threshold)
+
+
+verify_within(check_r1_again_prefers_trel_for_r2, 10)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-702-multi-radio-discover-by-rx.py b/tests/toranj/cli/test-702-multi-radio-discover-by-rx.py
new file mode 100755
index 0000000..8e4a396
--- /dev/null
+++ b/tests/toranj/cli/test-702-multi-radio-discover-by-rx.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: This test covers behavior of device after TREL network is temporarily disabled
+# and rediscovery of TREL by receiving message over that radio from the neighbor.
+#
+#   r1  ---------- r2
+# (15.4+trel)   (15.4+trel)
+#
+#  On r2 we disable trel temporarily.
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node(cli.RADIO_15_4_TREL)
+r2 = cli.Node(cli.RADIO_15_4_TREL)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Build network topology
+
+r1.form("discover-by-rx")
+r2.join(r1)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+high_preference_threshold = 220
+min_preference_threshold = 0
+
+verify(r1.multiradio_get_radios() == '[15.4, TREL]')
+verify(r2.multiradio_get_radios() == '[15.4, TREL]')
+
+r1_rloc = int(r1.get_rloc16(), 16)
+r2_rloc = int(r2.get_rloc16(), 16)
+
+r1_ml_addr = r1.get_mleid_ip_addr()
+r2_ml_addr = r2.get_mleid_ip_addr()
+
+# Wait till routes are discovered.
+
+
+def check_r1_router_table():
+    table = r1.get_router_table()
+    verify(len(table) == 2)
+    for entry in table:
+        verify(int(entry['RLOC16'], 0) == r1_rloc or int(entry['Link']) == 1)
+
+
+verify_within(check_r1_router_table, 120)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Verify that r1 detected both TREL & 15.4 as supported radios by r2
+
+
+def check_r1_sees_r2_has_two_radio_links():
+    neighbor_radios = r1.multiradio_get_neighbor_list()
+    verify(len(neighbor_radios) == 1)
+    info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0])
+    verify(int(info['RLOC16'], 16) == r2_rloc)
+    radios = info['Radios']
+    verify(len(radios) == 2)
+    verify('15.4' in radios)
+    verify('TREL' in radios)
+
+
+cli.verify_within(check_r1_sees_r2_has_two_radio_links, 10)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Ping r2 from r1 and verify that r1 prefers trel radio link for
+# sending to r2.
+
+r1.ping(r2_ml_addr, count=5)
+
+neighbor_radios = r1.multiradio_get_neighbor_list()
+verify(len(neighbor_radios) == 1)
+info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0])
+radios = info['Radios']
+verify(radios['TREL'] >= high_preference_threshold)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Now temporary filter trel link on r2 and ping again. We expect that
+# r1 to quickly detect that trel is no longer supported by r2 and
+# prefer 15.4 for tx to r2.
+
+r2.cli('trel filter enable')
+verify(r2.cli('trel filter')[0] == 'Enabled')
+
+r1.udp_open()
+for count in range(5):
+    r1.udp_send(r2.get_mleid_ip_addr(), 12345, 'hi_r2_from_r1')
+
+
+def check_r1_does_not_prefer_trel_for_r2():
+    neighbor_radios = r1.multiradio_get_neighbor_list()
+    verify(len(neighbor_radios) == 1)
+    info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0])
+    radios = info['Radios']
+    verify(radios['TREL'] <= min_preference_threshold)
+
+
+verify_within(check_r1_does_not_prefer_trel_for_r2, 10)
+
+# Check that we can send between r1 and r2 (now all tx should use 15.4)
+
+r1.ping(r2.get_mleid_ip_addr(), count=5, verify_success=False)
+r1.ping(r2.get_mleid_ip_addr(), count=5)
+
+neighbor_radios = r1.multiradio_get_neighbor_list()
+verify(len(neighbor_radios) == 1)
+info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0])
+radios = info['Radios']
+verify(radios['TREL'] <= min_preference_threshold)
+verify(radios['15.4'] >= high_preference_threshold)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Enable trel back on r2, start sending traffic from r2 to r1.
+# r2 would use probe mechanism to discover that trel is enabled again
+# r1 should notice new rx on trel and update trel preference for r1
+
+r2.cli('trel filter disable')
+verify(r2.cli('trel filter')[0] == 'Disabled')
+
+for count in range(80):
+    r1.udp_send(r2.get_mleid_ip_addr(), 12345, 'hi_r2_from_r1_probe')
+
+
+def check_r1_again_prefers_trel_for_r2():
+    neighbor_radios = r1.multiradio_get_neighbor_list()
+    verify(len(neighbor_radios) == 1)
+    info = cli.Node.parse_multiradio_neighbor_entry(neighbor_radios[0])
+    radios = info['Radios']
+    verify(radios['TREL'] >= high_preference_threshold)
+
+
+verify_within(check_r1_again_prefers_trel_for_r2, 10)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-703-multi-radio-mesh-header-msg.py b/tests/toranj/cli/test-703-multi-radio-mesh-header-msg.py
new file mode 100755
index 0000000..d202867
--- /dev/null
+++ b/tests/toranj/cli/test-703-multi-radio-mesh-header-msg.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: This test covers behavior of MeshHeader messages (message sent over multi-hop) when the
+# underlying devices (in the path) support different radio types with different frame MTU length.
+#
+#   r1  --------- r2 ---------- r3
+# (trel)      (15.4+trel)     (15.4)
+#                  |
+#                  |
+#               c2 (15.4)
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node(cli.RADIO_TREL)
+r2 = cli.Node(cli.RADIO_15_4_TREL)
+r3 = cli.Node(cli.RADIO_15_4)
+c2 = cli.Node(cli.RADIO_15_4)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Build network topology
+
+r1.allowlist_node(r2)
+
+r2.allowlist_node(r1)
+r2.allowlist_node(r3)
+r2.allowlist_node(c2)
+
+r3.allowlist_node(r2)
+
+c2.allowlist_node(r2)
+
+r1.form("mesh-header")
+
+r2.join(r1)
+r3.join(r2)
+c2.join(r2, cli.JOIN_TYPE_END_DEVICE)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+verify(r3.get_state() == 'router')
+verify(c2.get_state() == 'child')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+verify(r1.multiradio_get_radios() == '[TREL]')
+verify(r2.multiradio_get_radios() == '[15.4, TREL]')
+verify(r3.multiradio_get_radios() == '[15.4]')
+
+r1_rloc = int(r1.get_rloc16(), 16)
+
+r1_ml_addr = r1.get_mleid_ip_addr()
+r3_ml_addr = r3.get_mleid_ip_addr()
+
+# Wait till routes are discovered.
+
+
+def check_r1_router_table():
+    table = r1.get_router_table()
+    verify(len(table) == 3)
+    for entry in table:
+        verify(int(entry['RLOC16'], 0) == r1_rloc or int(entry['Link']) == 1 or int(entry['Next Hop']) != 63)
+
+
+verify_within(check_r1_router_table, 120)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Ping r3 from r2 using large message size. Such a message would be sent as a
+# Mesh Header message. Note that the origin r1 is a trel-only device
+# whereas the final destination r3 is a 15.4-only device. The originator
+# should use smaller MTU size (which then fragment the message into
+# multiple frames) otherwise the 15.4 link won't be able to handle it.
+
+r1.ping(r3_ml_addr, size=1000, count=2)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Ping r1 from c2 using large message size.
+
+c2.ping(r1_ml_addr, size=1000, count=2)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Ping r1 from r3
+
+r3.ping(r1_ml_addr, size=1000, count=2)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Finally ping r1 and r3 from r2.
+
+r2.ping(r1_ml_addr, size=1000, count=2)
+r2.ping(r3_ml_addr, size=1000, count=2)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-704-multi-radio-scan.py b/tests/toranj/cli/test-704-multi-radio-scan.py
new file mode 100755
index 0000000..697f08d
--- /dev/null
+++ b/tests/toranj/cli/test-704-multi-radio-scan.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: Test active scan with nodes supporting different radios
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+n1 = cli.Node(cli.RADIO_15_4)
+n2 = cli.Node(cli.RADIO_TREL)
+n3 = cli.Node(cli.RADIO_15_4_TREL)
+s1 = cli.Node(cli.RADIO_15_4)
+s2 = cli.Node(cli.RADIO_TREL)
+s3 = cli.Node(cli.RADIO_15_4_TREL)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Build network topology
+
+n1.form("n1", channel='20')
+n2.form("n2", channel='21')
+n3.form("n3", channel='22')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+verify(n1.multiradio_get_radios() == '[15.4]')
+verify(n2.multiradio_get_radios() == '[TREL]')
+verify(n3.multiradio_get_radios() == '[15.4, TREL]')
+verify(s1.multiradio_get_radios() == '[15.4]')
+verify(s2.multiradio_get_radios() == '[TREL]')
+verify(s3.multiradio_get_radios() == '[15.4, TREL]')
+
+
+def verify_scan_result_conatins_nodes(scan_result, nodes):
+    table = cli.Node.parse_table(scan_result)
+    verify(len(table) >= len(nodes))
+    for node in nodes:
+        ext_addr = node.get_ext_addr()
+        for entry in table:
+            if entry['MAC Address'] == ext_addr:
+                verify(int(entry['PAN'], 16) == int(node.get_panid(), 16))
+                verify(int(entry['Ch']) == int(node.get_channel()))
+                break
+        else:
+            verify(False)
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Scan by scanner nodes (no network)
+
+# Scan by s1 (15.4 only), expect to see n1(15.4) and n3(15.4+trel)
+
+verify_scan_result_conatins_nodes(s1.cli('scan'), [n1, n3])
+
+# Scan by s2 (trel only), expect to see n2(trel) and n3(15.4+trel)
+verify_scan_result_conatins_nodes(s2.cli('scan'), [n2, n3])
+
+# Scan by s3 (trel+15.4), expect to see all nodes
+verify_scan_result_conatins_nodes(s3.cli('scan'), [n1, n2, n3])
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-705-multi-radio-discover-scan.py b/tests/toranj/cli/test-705-multi-radio-discover-scan.py
new file mode 100755
index 0000000..4206280
--- /dev/null
+++ b/tests/toranj/cli/test-705-multi-radio-discover-scan.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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: Test MLE discover scan with nodes supporting different radios
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 10
+cli.Node.set_time_speedup_factor(speedup)
+
+n1 = cli.Node(cli.RADIO_15_4)
+n2 = cli.Node(cli.RADIO_TREL)
+n3 = cli.Node(cli.RADIO_15_4_TREL)
+s1 = cli.Node(cli.RADIO_15_4)
+s2 = cli.Node(cli.RADIO_TREL)
+s3 = cli.Node(cli.RADIO_15_4_TREL)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Build network topology
+
+n1.form("n1", channel='20')
+n2.form("n2", channel='21')
+n3.form("n3", channel='22')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+verify(n1.multiradio_get_radios() == '[15.4]')
+verify(n2.multiradio_get_radios() == '[TREL]')
+verify(n3.multiradio_get_radios() == '[15.4, TREL]')
+verify(s1.multiradio_get_radios() == '[15.4]')
+verify(s2.multiradio_get_radios() == '[TREL]')
+verify(s3.multiradio_get_radios() == '[15.4, TREL]')
+
+
+def verify_scan_result_conatins_nodes(scan_result, nodes):
+    table = cli.Node.parse_table(scan_result)
+    verify(len(table) >= len(nodes))
+    for node in nodes:
+        ext_addr = node.get_ext_addr()
+        for entry in table:
+            if entry['MAC Address'] == ext_addr:
+                verify(int(entry['PAN'], 16) == int(node.get_panid(), 16))
+                verify(int(entry['Ch']) == int(node.get_channel()))
+                break
+        else:
+            verify(False)
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Scan by scanner nodes (no network)
+
+s1.interface_up()
+s2.interface_up()
+s3.interface_up()
+
+# Scan by s1 (15.4 only), expect to see n1(15.4) and n3(15.4+trel)
+
+verify_scan_result_conatins_nodes(s1.cli('discover'), [n1, n3])
+
+# Scan by s2 (trel only), expect to see n2(trel) and n3(15.4+trel)
+verify_scan_result_conatins_nodes(s2.cli('discover'), [n2, n3])
+
+# Scan by s3 (trel+15.4), expect to see all nodes
+verify_scan_result_conatins_nodes(s3.cli('discover'), [n1, n2, n3])
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Scan by the nodes
+
+# Scan by n1 (15.4 only), expect to see only n3(15.4+trel)
+verify_scan_result_conatins_nodes(n1.cli('discover'), [n3])
+
+# Scan by n2 (trel only), expect to see only n3(15.4+trel)
+verify_scan_result_conatins_nodes(n2.cli('discover'), [n3])
+
+# Scan by n3 (15.4+trel), expect to see n1(15.4) and n2(trel)
+verify_scan_result_conatins_nodes(n3.cli('discover'), [n1, n2])
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/ncp/test-017-parent-reset-child-recovery.py b/tests/toranj/ncp/test-017-parent-reset-child-recovery.py
index ede43dc..e6ff934 100644
--- a/tests/toranj/ncp/test-017-parent-reset-child-recovery.py
+++ b/tests/toranj/ncp/test-017-parent-reset-child-recovery.py
@@ -148,7 +148,7 @@
     verify(parent.is_associated())
 
 
-wpan.verify_within(check_parent_is_associated, 10)
+wpan.verify_within(check_parent_is_associated, 40)
 
 # Verify that all the children are recovered and present in the parent's
 # child table again.
diff --git a/tests/toranj/ncp/test-018-child-supervision.py b/tests/toranj/ncp/test-018-child-supervision.py
deleted file mode 100644
index 2fe2767..0000000
--- a/tests/toranj/ncp/test-018-child-supervision.py
+++ /dev/null
@@ -1,180 +0,0 @@
-#!/usr/bin/env python3
-#
-#  Copyright (c) 2018, 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.
-
-import time
-import wpan
-from wpan import verify
-
-# -----------------------------------------------------------------------------------------------------------------------
-# Test description: Child Supervision feature
-#
-# This test covers the behavior of Child Supervision feature.
-#
-# This test uses MAC allowlisting to emulate the situation where a child is
-# removed from parent's child table while the child continues to stay attached
-# to the parent (since data polls from child are acked at radio platform layer).
-# Specifically the test verifies that once supervision check is enabled on the
-# child, the child detects that it is no longer present in the parent's table
-# and tries to re-attach.
-
-# The test verifies the behavior of both parent and child, when supervision is
-# enabled. It verifies that parent is periodically sending supervision messages
-# to the child and that the child is monitoring the messages.
-#
-# This test also indirectly verifies the child timeout on parent.
-#
-
-test_name = __file__[:-3] if __file__.endswith('.py') else __file__
-print('-' * 120)
-print('Starting \'{}\''.format(test_name))
-
-# -----------------------------------------------------------------------------------------------------------------------
-# Creating `wpan.Nodes` instances
-
-speedup = 2
-wpan.Node.set_time_speedup_factor(speedup)
-
-parent = wpan.Node()
-child = wpan.Node()
-
-# -----------------------------------------------------------------------------------------------------------------------
-# Init all nodes
-
-wpan.Node.init_all_nodes()
-
-# -----------------------------------------------------------------------------------------------------------------------
-# Build network topology
-
-CHILD_TIMEOUT = 6
-CHILD_SUPERVISION_CHECK_TIMEOUT = 2
-PARENT_SUPERVISION_INTERVAL = 1
-
-child.set(wpan.WPAN_POLL_INTERVAL, '500')
-child.set(wpan.WPAN_THREAD_CHILD_TIMEOUT, str(CHILD_TIMEOUT))
-
-parent.form("child-sup")
-child.join_node(parent, wpan.JOIN_TYPE_SLEEPY_END_DEVICE)
-
-# -----------------------------------------------------------------------------------------------------------------------
-# Test implementation
-
-# Disable child supervision on child and parent
-parent.set(wpan.WPAN_CHILD_SUPERVISION_INTERVAL, '0')
-child.set(wpan.WPAN_CHILD_SUPERVISION_CHECK_TIMEOUT, '0')
-verify(int(parent.get(wpan.WPAN_CHILD_SUPERVISION_INTERVAL), 0) == 0)
-verify(int(child.get(wpan.WPAN_CHILD_SUPERVISION_CHECK_TIMEOUT), 0) == 0)
-
-# Check that the child is associated and has correct timeout
-verify(child.is_associated())
-verify(int(child.get(wpan.WPAN_THREAD_CHILD_TIMEOUT), 0) == CHILD_TIMEOUT)
-
-# Verify the child table on parent contains the child with correct timeout
-child_table = wpan.parse_child_table_result(parent.get(wpan.WPAN_THREAD_CHILD_TABLE))
-verify(len(child_table) == 1)
-verify(int(child_table[0].timeout, 0) == CHILD_TIMEOUT)
-
-time.sleep(1)
-
-# Enabling allowlisting on parent
-#
-# Since child is not in parent's allowlist, the data polls from child
-# should be rejected and the child should be removed from parent's
-# child table after timeout. The child however should continue to
-# stay attached (since data polls are acked by radio driver) and
-# supervision check is disabled on the child.
-
-parent.set(wpan.WPAN_MAC_ALLOWLIST_ENABLED, '1')
-
-
-def check_child_is_removed_from_parent_child_table():
-    child_table = wpan.parse_child_table_result(parent.get(wpan.WPAN_THREAD_CHILD_TABLE))
-    verify(len(child_table) == 0)
-
-
-# wait till child is removed from parent's child table
-# after this child should still be associated
-wpan.verify_within(check_child_is_removed_from_parent_child_table, CHILD_TIMEOUT / speedup + 2)
-verify(child.is_associated())
-
-# Enable supervision check on child and expect the child to
-# become detached after the check timeout
-
-child.set(
-    wpan.WPAN_CHILD_SUPERVISION_CHECK_TIMEOUT,
-    str(CHILD_SUPERVISION_CHECK_TIMEOUT),
-)
-
-
-def check_child_is_detached():
-    verify(not child.is_associated())
-
-
-wpan.verify_within(check_child_is_detached, CHILD_SUPERVISION_CHECK_TIMEOUT / speedup + 8)
-
-# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-# Enable child supervision on parent and disable allowlisting
-
-parent.set(wpan.WPAN_CHILD_SUPERVISION_INTERVAL, str(PARENT_SUPERVISION_INTERVAL))
-parent.set(wpan.WPAN_MAC_ALLOWLIST_ENABLED, '0')
-
-# Wait for the child to attach back
-
-
-def check_child_is_attached():
-    verify(child.is_associated())
-
-
-wpan.verify_within(check_child_is_attached, 5)
-
-# MAC counters are used to verify the child supervision behavior.
-
-parent_unicast_tx_count = int(parent.get("NCP:Counter:TX_PKT_UNICAST"), 0)
-
-time.sleep(PARENT_SUPERVISION_INTERVAL * 1.2 / speedup)
-
-# To verify that the parent is indeed sending empty "supervision"
-# messages to its child, MAC counter for number of unicast tx is
-# used. Note that supervision interval on parent is set to 1 sec.
-
-verify(int(parent.get("NCP:Counter:TX_PKT_UNICAST"), 0) >= parent_unicast_tx_count + 1)
-
-verify(child.is_associated())
-
-# Disable child supervision on parent
-parent.set(wpan.WPAN_CHILD_SUPERVISION_INTERVAL, '0')
-
-time.sleep(CHILD_SUPERVISION_CHECK_TIMEOUT * 3 / speedup)
-verify(child.is_associated())
-
-# -----------------------------------------------------------------------------------------------------------------------
-# Test finished
-
-wpan.Node.finalize_all_nodes()
-
-print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/ncp/test-027-child-mode-change.py b/tests/toranj/ncp/test-027-child-mode-change.py
index b3c92c6..8301cd8 100644
--- a/tests/toranj/ncp/test-027-child-mode-change.py
+++ b/tests/toranj/ncp/test-027-child-mode-change.py
@@ -97,6 +97,7 @@
 # Test implementation
 
 WAIT_INTERVAL = 6
+LEADER_RESET_DELAY = 38
 
 # Thread Mode for end-device and sleepy end-device
 DEVICE_MODE_SLEEPY_END_DEVICE = (wpan.THREAD_MODE_FLAG_FULL_NETWORK_DATA)
@@ -127,8 +128,6 @@
 
 sender = parent.prepare_tx(parent_ml_address, child2_ml_address, 800, NUM_MSGS)
 
-child2_rx_ip_counter = int(child2.get(wpan.WPAN_NCP_COUNTER_RX_IP_SEC_TOTAL), 0)
-
 wpan.Node.perform_async_tx_rx()
 
 verify(sender.was_successful)
@@ -144,16 +143,9 @@
 # Verify that the child table on parent is also updated
 wpan.verify_within(check_child_table, WAIT_INTERVAL)
 
-
-def check_child2_received_msg():
-    verify(int(child2.get(wpan.WPAN_NCP_COUNTER_RX_IP_SEC_TOTAL), 0) >= child2_rx_ip_counter + NUM_MSGS)
-
-
-wpan.verify_within(check_child2_received_msg, WAIT_INTERVAL)
-
 # Reset parent and verify all children are recovered
 parent.reset()
-wpan.verify_within(check_child_table, WAIT_INTERVAL)
+wpan.verify_within(check_child_table, WAIT_INTERVAL + LEADER_RESET_DELAY)
 
 # -----------------------------------------------------------------------------------------------------------------------
 # Test finished
diff --git a/tests/toranj/ncp/test-600-channel-manager-properties.py b/tests/toranj/ncp/test-600-channel-manager-properties.py
index a222d09..c99ec38 100644
--- a/tests/toranj/ncp/test-600-channel-manager-properties.py
+++ b/tests/toranj/ncp/test-600-channel-manager-properties.py
@@ -101,7 +101,7 @@
 node.reset()
 
 start_time = time.time()
-wait_time = 20
+wait_time = 50
 
 while node.get(wpan.WPAN_STATE) != wpan.STATE_ASSOCIATED:
     if time.time() - start_time > wait_time:
diff --git a/tests/toranj/openthread-core-toranj-config-simulation.h b/tests/toranj/openthread-core-toranj-config-simulation.h
index 09d925d..04d6d0e 100644
--- a/tests/toranj/openthread-core-toranj-config-simulation.h
+++ b/tests/toranj/openthread-core-toranj-config-simulation.h
@@ -65,7 +65,23 @@
  * Selects if, and where the LOG output goes to.
  *
  */
-#define OPENTHREAD_CONFIG_LOG_OUTPUT OPENTHREAD_CONFIG_LOG_OUTPUT_APP
+#define OPENTHREAD_CONFIG_LOG_OUTPUT OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED
+
+/**
+ * @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.
+ *
+ */
+#define OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE 1
+
+/**
+ * @def OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL
+ *
+ * Defines the log level to use when CLI emits its command input/output to the logs.
+ *
+ */
+#define OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_LEVEL OT_LOG_LEVEL_INFO
 
 /**
  * @def OPENTHREAD_CONFIG_DNS_DSO_ENABLE
@@ -75,4 +91,12 @@
  */
 #define OPENTHREAD_CONFIG_DNS_DSO_ENABLE 1
 
+/**
+ * @def OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
+ *
+ * Enable the external heap.
+ *
+ */
+#define OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE 1
+
 #endif /* OPENTHREAD_CORE_TORANJ_CONFIG_SIMULATION_H_ */
diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h
index 508f605..1812c81 100644
--- a/tests/toranj/openthread-core-toranj-config.h
+++ b/tests/toranj/openthread-core-toranj-config.h
@@ -76,6 +76,22 @@
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 1
 
 /**
+ * @def OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
+ *
+ * Define as 1 to enable IPv6 Border Routing counters.
+ *
+ */
+#define OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE 1
+
+/**
+ * @def OPENTHREAD_CONFIG_MESH_DIAG_ENABLE
+ *
+ * Define as 1 to enable Mesh Diagnostics module.
+ *
+ */
+#define OPENTHREAD_CONFIG_MESH_DIAG_ENABLE 1
+
+/**
  * @def OPENTHREAD_CONFIG_COMMISSIONER_ENABLE
  *
  * Define to 1 to enable Commissioner support.
@@ -140,14 +156,6 @@
 #define OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE 1
 
 /**
- * @def OPENTHREAD_CONFIG_LEGACY_ENABLE
- *
- * Define to 1 to enable legacy network support.
- *
- */
-#define OPENTHREAD_CONFIG_LEGACY_ENABLE 1
-
-/**
  * @def OPENTHREAD_CONFIG_ECDSA_ENABLE
  *
  * Define to 1 to enable ECDSA support.
@@ -436,14 +444,6 @@
 #define OPENTHREAD_CONFIG_CHANNEL_MANAGER_THRESHOLD_TO_CHANGE_CHANNEL (0xffff * 10 / 100)
 
 /**
- * @def OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
- *
- * Define to 1 to enable Child Supervision support.
- *
- */
-#define OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE 1
-
-/**
  * @def OPENTHREAD_CONFIG_TMF_PENDING_DATASET_MINIMUM_DELAY
  *
  * Minimum Delay Timer value for a Pending Operational Dataset (in ms).
@@ -538,6 +538,71 @@
  */
 #define OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE 1
 
+/**
+ * @def OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
+ *
+ * Define to 1 to enable Backbone Router support.
+ *
+ */
+#define OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE 1
+
+/**
+ * @def OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK
+ *
+ * Define as 1 to have CLI register an IPv6 receive callback using `otIp6SetReceiveCallback()`.
+ *
+ * This is intended for testing only. Receive callback should be registered for the `otIp6GetBorderRoutingCounters()`
+ * to count the messages being passed to the callback.
+ *
+ */
+#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 3831afc..2bc8fa9 100755
--- a/tests/toranj/start.sh
+++ b/tests/toranj/start.sh
@@ -39,32 +39,18 @@
     sudo rm tmp/*.flash tmp/*.data tmp/*.swap >/dev/null 2>&1
     sudo rm ./*.log >/dev/null 2>&1
 
-    # Clear any wpantund instances
-    sudo killall wpantund >/dev/null 2>&1
+    if [ "$TORANJ_CLI" != 1 ]; then
+        # Clear any wpantund instances
+        sudo killall wpantund >/dev/null 2>&1
 
-    while read -r interface; do
-        sudo ip link delete "$interface" >/dev/null 2>&1
-    done < <(ifconfig 2>/dev/null | grep -o "wpan[0-9]*")
-
-    sudo ip address flush trel
+        while read -r interface; do
+            sudo ip link delete "$interface" >/dev/null 2>&1
+        done < <(ifconfig 2>/dev/null | grep -o "wpan[0-9]*")
+    fi
 
     sleep 0.3
 }
 
-prepare_trel_link()
-{
-    # Prepares a netif "trel" as virtual eth to use for
-    # testing "TREL" radio link in POSIX platform.
-
-    echo "Preparing trel netif"
-    sudo ip link delete trel
-    sudo ip link add trel type veth peer name trel-peer || die
-    sudo ip link set trel multicast on || die
-    sudo ip link set trel up || die
-    sudo ip link set trel-peer multicast on || die
-    sudo ip link set trel-peer up || die
-}
-
 run()
 {
     counter=0
@@ -89,8 +75,52 @@
     done
 }
 
+install_wpantund()
+{
+    echo "Installing wpantund"
+    sudo apt-get --no-install-recommends install -y dbus libdbus-1-dev
+    sudo apt-get --no-install-recommends install -y autoconf-archive
+    sudo apt-get --no-install-recommends install -y libarchive-tools
+    sudo apt-get --no-install-recommends install -y libtool
+    sudo apt-get --no-install-recommends install -y libglib2.0-dev
+    sudo apt-get --no-install-recommends install -y lcov
+
+    sudo add-apt-repository universe
+    sudo apt-get update
+    sudo apt-get --no-install-recommends install -y libboost-all-dev python2
+
+    git clone --depth=1 --branch=master https://github.com/openthread/wpantund.git || die "wpandtund clone"
+    cd wpantund || die "cd wpantund failed"
+    ./bootstrap.sh
+    ./configure
+    sudo make -j2 || die "wpantund make failed"
+    sudo make install || die "wpantund make install failed"
+    cd .. || die "cd .. failed"
+}
+
 cd "$(dirname "$0")" || die "cd failed"
 
+if [ "$TORANJ_NCP" = 1 ]; then
+    echo "========================================================================"
+    echo "Running toranj-ncp triggered by event ${TORANJ_EVENT_NAME}"
+
+    if [ "$TORANJ_EVENT_NAME" = "pull_request" ]; then
+        cd ../..
+        OT_SHA_OLD="$(git cat-file -p HEAD | grep 'parent ' | head -n1 | cut -d' ' -f2)"
+        git fetch --depth 1 --no-recurse-submodules origin "${OT_SHA_OLD}"
+        if git diff --name-only --exit-code "${OT_SHA_OLD}" -- src/ncp; then
+            echo "No changes to any of src/ncp files - skip running tests."
+            echo "========================================================================"
+            exit 0
+        fi
+        cd tests/toranj || die "cd tests/toranj failed"
+        echo "There are change in src/ncp files, running toranj-ncp tests"
+    fi
+
+    echo "========================================================================"
+    install_wpantund
+fi
+
 if [ -z "${top_builddir}" ]; then
     top_builddir=.
 fi
@@ -107,18 +137,15 @@
     log_file_name="ot-logs"
 else
     app_name="ncp"
-    python_app="python"
+    python_app="python2"
     log_file_name="wpantund-logs"
 fi
 
 if [ "$TORANJ_RADIO" = "multi" ]; then
     # Build all combinations
     ./build.sh "${coverage_option}" "${app_name}"-15.4 || die "${app_name}-15.4 build failed"
-    (cd ${top_builddir} && make clean) || die "cd and clean failed"
     ./build.sh "${coverage_option}" "${app_name}"-trel || die "${app_name}-trel build failed"
-    (cd ${top_builddir} && make clean) || die "cd and clean failed"
     ./build.sh "${coverage_option}" "${app_name}"-15.4+trel || die "${app_name}-15.4+trel build failed"
-    (cd ${top_builddir} && make clean) || die "cd and clean failed"
 else
     ./build.sh "${coverage_option}" "${app_name}"-"${TORANJ_RADIO}" || die "build failed"
 fi
@@ -126,10 +153,46 @@
 cleanup
 
 if [ "$TORANJ_CLI" = 1 ]; then
+
+    if [ "$TORANJ_RADIO" = "multi" ]; then
+        run cli/test-700-multi-radio-join.py
+        run cli/test-701-multi-radio-probe.py
+        run cli/test-702-multi-radio-discover-by-rx.py
+        run cli/test-703-multi-radio-mesh-header-msg.py
+        run cli/test-704-multi-radio-scan.py
+        run cli/test-705-multi-radio-discover-scan.py
+
+        exit 0
+    fi
+
     run cli/test-001-get-set.py
     run cli/test-002-form.py
     run cli/test-003-join.py
+    run cli/test-004-scan.py
+    run cli/test-005-traffic-router-to-child.py
+    run cli/test-006-traffic-multi-hop.py
+    run cli/test-007-off-mesh-route-traffic.py
+    run cli/test-008-multicast-traffic.py
+    run cli/test-009-router-table.py
+    run cli/test-010-partition-merge.py
+    run cli/test-011-network-data-timeout.py
+    run cli/test-012-reset-recovery.py
+    run cli/test-013-address-cache-parent-switch.py
+    run cli/test-014-address-resolver.py
+    run cli/test-015-clear-addresss-cache-for-sed.py
+    run cli/test-016-child-mode-change.py
+    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
+    # requires energy scan which is not supported in this case.
+    if [ "$TORANJ_RADIO" != "trel" ]; then
+        run cli/test-602-channel-manager-channel-select.py
+    fi
+    run cli/test-603-channel-announce-recovery.py
 
     exit 0
 fi
@@ -161,7 +224,6 @@
 run ncp/test-015-same-prefix-on-multiple-nodes.py
 run ncp/test-016-neighbor-table.py
 run ncp/test-017-parent-reset-child-recovery.py
-run ncp/test-018-child-supervision.py
 run ncp/test-019-inform-previous-parent.py
 run ncp/test-020-router-table.py
 run ncp/test-021-address-cache-table.py
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 9a68ecf..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
@@ -60,11 +59,14 @@
 )
 
 set(COMMON_LIBS
+    openthread-spinel-ncp
+    openthread-hdlc
     ot-test-platform
     openthread-ftd
     ot-test-platform
     ${OT_MBEDTLS}
     ot-config
+    openthread-ftd
 )
 
 add_executable(ot-test-aes
@@ -255,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
 )
@@ -662,6 +685,27 @@
 
 add_test(NAME ot-test-message-queue COMMAND ot-test-message-queue)
 
+add_executable(ot-test-mle
+    test_mle.cpp
+)
+
+target_include_directories(ot-test-mle
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+
+target_compile_options(ot-test-mle
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+
+target_link_libraries(ot-test-mle
+    PRIVATE
+        ${COMMON_LIBS}
+)
+
+add_test(NAME ot-test-mle COMMAND ot-test-mle)
+
 add_executable(ot-test-multicast-listeners-table
     test_multicast_listeners_table.cpp
 )
@@ -683,6 +727,27 @@
 
 add_test(NAME ot-test-multicast-listeners-table COMMAND ot-test-multicast-listeners-table)
 
+add_test(NAME ot-test-nat64 COMMAND ot-test-nat64)
+
+add_executable(ot-test-nat64
+    test_nat64.cpp
+)
+
+target_include_directories(ot-test-nat64
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+
+target_compile_options(ot-test-nat64
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+
+target_link_libraries(ot-test-nat64
+    PRIVATE
+        ${COMMON_LIBS}
+)
+
 add_executable(ot-test-ndproxy-table
     test_ndproxy_table.cpp
 )
@@ -790,6 +855,27 @@
 
 add_test(NAME ot-test-pool COMMAND ot-test-pool)
 
+add_executable(ot-test-power-calibration
+    test_power_calibration.cpp
+)
+
+target_include_directories(ot-test-power-calibration
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+
+target_compile_options(ot-test-power-calibration
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+
+target_link_libraries(ot-test-power-calibration
+    PRIVATE
+        ${COMMON_LIBS}
+)
+
+add_test(NAME ot-test-power-calibration COMMAND ot-test-power-calibration)
+
 add_executable(ot-test-priority-queue
     test_priority_queue.cpp
 )
@@ -916,6 +1002,28 @@
 
 add_test(NAME ot-test-serial-number COMMAND ot-test-serial-number)
 
+add_executable(ot-test-srp-server
+    test_srp_server.cpp
+)
+
+target_include_directories(ot-test-srp-server
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+
+target_compile_options(ot-test-srp-server
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+
+target_link_libraries(ot-test-srp-server
+    PRIVATE
+        ${COMMON_LIBS}
+)
+
+add_test(NAME ot-test-srp-server COMMAND ot-test-srp-server)
+
+
 add_executable(ot-test-string
     test_string.cpp
 )
@@ -941,6 +1049,22 @@
     test_timer.cpp
 )
 
+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
     PRIVATE
         ${COMMON_INCLUDES}
@@ -957,3 +1081,109 @@
 )
 
 add_test(NAME ot-test-timer COMMAND ot-test-timer)
+
+add_executable(ot-test-tlv
+    test_tlv.cpp
+)
+
+target_include_directories(ot-test-tlv
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+
+target_compile_options(ot-test-tlv
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+
+target_link_libraries(ot-test-tlv
+    PRIVATE
+        ${COMMON_LIBS}
+)
+
+add_test(NAME ot-test-tlv COMMAND ot-test-tlv)
+
+add_executable(ot-test-hdlc
+    test_hdlc.cpp
+)
+target_include_directories(ot-test-hdlc
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+target_compile_options(ot-test-hdlc
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+target_link_libraries(ot-test-hdlc
+    PRIVATE
+        ${COMMON_LIBS}
+)
+add_test(NAME ot-test-hdlc COMMAND ot-test-hdlc)
+
+add_executable(ot-test-spinel-buffer
+    test_spinel_buffer.cpp
+)
+target_include_directories(ot-test-spinel-buffer
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+target_compile_options(ot-test-spinel-buffer
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+target_link_libraries(ot-test-spinel-buffer
+    PRIVATE
+        ${COMMON_LIBS}
+)
+add_test(NAME ot-test-spinel-buffer COMMAND ot-test-spinel-buffer)
+
+add_executable(ot-test-spinel-decoder
+    test_spinel_decoder.cpp
+)
+target_include_directories(ot-test-spinel-decoder
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+target_compile_options(ot-test-spinel-decoder
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+target_link_libraries(ot-test-spinel-decoder
+    PRIVATE
+        ${COMMON_LIBS}
+)
+add_test(NAME ot-test-spinel-decoder COMMAND ot-test-spinel-decoder)
+
+add_executable(ot-test-spinel-encoder
+    test_spinel_encoder.cpp
+)
+target_include_directories(ot-test-spinel-encoder
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+target_compile_options(ot-test-spinel-encoder
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+target_link_libraries(ot-test-spinel-encoder
+    PRIVATE
+        ${COMMON_LIBS}
+)
+add_test(NAME ot-test-spinel-encoder COMMAND ot-test-spinel-encoder)
+
+add_executable(ot-test-address-sanitizer
+    test_address_sanitizer.cpp
+)
+target_include_directories(ot-test-address-sanitizer
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+target_compile_options(ot-test-address-sanitizer
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+target_link_libraries(ot-test-address-sanitizer
+    PRIVATE
+        ${COMMON_LIBS}
+)
+add_test(NAME ot-test-address-sanitizer COMMAND ot-test-address-sanitizer)
diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am
deleted file mode 100644
index 21316aa..0000000
--- a/tests/unit/Makefile.am
+++ /dev/null
@@ -1,389 +0,0 @@
-#
-#  Copyright (c) 2016, 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
-
-COMMON_LIBTOOLFLAGS = --preserve-dup-deps
-
-#
-# Local headers to build against and distribute but not to install
-# since they are not part of the package.
-#
-noinst_HEADERS                                                      = \
-    test_lowpan.hpp                                                   \
-    test_platform.h                                                   \
-    test_util.h                                                       \
-    test_util.hpp                                                     \
-    $(NULL)
-
-#
-# Other files we do want to distribute with the package.
-#
-EXTRA_DIST                                                          = \
-    $(NULL)
-
-if OPENTHREAD_BUILD_TESTS
-# C preprocessor option flags that will apply to all compiled objects in this
-# makefile.
-
-AM_CPPFLAGS                                                         = \
-    -DOPENTHREAD_FTD=1                                                \
-    -DOPENTHREAD_MTD=0                                                \
-    -DOPENTHREAD_RADIO=0                                              \
-    -DOPENTHREAD_RADIO_CLI=0                                          \
-    -DOPENTHREAD_SPINEL_CONFIG_OPENTHREAD_MESSAGE_ENABLE=1            \
-    -I$(top_srcdir)/include                                           \
-    -I$(top_srcdir)/src                                               \
-    -I$(top_srcdir)/src/core                                          \
-    $(NULL)
-
-if OPENTHREAD_EXAMPLES_SIMULATION
-AM_CPPFLAGS                                                        += \
-    -I$(top_srcdir)/examples/platforms                                \
-    $(NULL)
-endif
-
-if OPENTHREAD_PLATFORM_POSIX
-AM_CPPFLAGS                                                        += \
-    -DOPENTHREAD_PLATFORM_POSIX=1                                     \
-    -I$(top_srcdir)/src/posix/platform                                \
-    $(NULL)
-endif
-
-COMMON_LDADD                                                        = \
-    $(NULL)
-
-if OPENTHREAD_ENABLE_NCP
-COMMON_LDADD                                                       += \
-    $(top_builddir)/src/ncp/libopenthread-ncp-ftd.a                   \
-    $(NULL)
-endif
-
-COMMON_LDADD                                                       += \
-    $(top_builddir)/src/core/libopenthread-ftd.a                      \
-    $(top_builddir)/third_party/tcplp/libtcplp.a                      \
-    $(top_builddir)/src/core/libopenthread-ftd.a                      \
-    -lpthread                                                         \
-    $(NULL)
-
-if OPENTHREAD_ENABLE_BUILTIN_MBEDTLS
-COMMON_LDADD                                                       += \
-    $(top_builddir)/third_party/mbedtls/libmbedcrypto.a               \
-    $(NULL)
-endif
-
-if OPENTHREAD_PLATFORM_POSIX
-COMMON_LDADD                                                       += \
-    -lutil
-    $(NULL)
-endif
-
-# Test applications that should be run when the 'check' target is run.
-
-check_PROGRAMS                                                      = \
-    ot-test-toolchain                                                 \
-    $(NULL)
-
-if OPENTHREAD_ENABLE_FTD
-check_PROGRAMS                                                     += \
-    ot-test-aes                                                       \
-    ot-test-array                                                     \
-    ot-test-binary-search                                             \
-    ot-test-checksum                                                  \
-    ot-test-child                                                     \
-    ot-test-child-table                                               \
-    ot-test-cmd-line-parser                                           \
-    ot-test-data                                                      \
-    ot-test-dns                                                       \
-    ot-test-dso                                                       \
-    ot-test-ecdsa                                                     \
-    ot-test-flash                                                     \
-    ot-test-frame-builder                                             \
-    ot-test-heap                                                      \
-    ot-test-heap-array                                                \
-    ot-test-heap-string                                               \
-    ot-test-hkdf-sha256                                               \
-    ot-test-hmac-sha256                                               \
-    ot-test-ip4-header                                                \
-    ot-test-ip6-header                                                \
-    ot-test-ip-address                                                \
-    ot-test-link-quality                                              \
-    ot-test-linked-list                                               \
-    ot-test-lowpan                                                    \
-    ot-test-mac-frame                                                 \
-    ot-test-macros                                                    \
-    ot-test-meshcop                                                   \
-    ot-test-message                                                   \
-    ot-test-message-queue                                             \
-    ot-test-multicast-listeners-table                                 \
-    ot-test-ndproxy-table                                             \
-    ot-test-netif                                                     \
-    ot-test-network-data                                              \
-    ot-test-network-name                                              \
-    ot-test-pool                                                      \
-    ot-test-priority-queue                                            \
-    ot-test-pskc                                                      \
-    ot-test-serial-number                                             \
-    ot-test-routing-manager                                           \
-    ot-test-smart-ptrs                                                \
-    ot-test-string                                                    \
-    ot-test-timer                                                     \
-    $(NULL)
-
-if OPENTHREAD_ENABLE_NCP
-check_PROGRAMS                                                     += \
-    ot-test-hdlc                                                      \
-    ot-test-spinel-buffer                                             \
-    ot-test-spinel-decoder                                            \
-    ot-test-spinel-encoder                                            \
-    $(NULL)
-endif
-endif # OPENTHREAD_ENABLE_FTD
-
-XFAIL_TESTS                                                         = \
-    $(NULL)
-
-if OPENTHREAD_WITH_ADDRESS_SANITIZER
-check_PROGRAMS                    += ot-test-address-sanitizer
-XFAIL_TESTS                       += ot-test-address-sanitizer
-ot_test_address_sanitizer_SOURCES  = test_address_sanitizer.cpp
-endif # OPENTHREAD_WITH_ADDRESS_SANITIZER
-
-
-# Test applications and scripts that should be built and run when the
-# 'check' target is run.
-
-TESTS                                                               = \
-    $(check_PROGRAMS)                                                 \
-    $(NULL)
-
-# The additional environment variables and their values that will be
-# made available to all programs and scripts in TESTS.
-
-TESTS_ENVIRONMENT                                                   = \
-    top_srcdir='$(top_srcdir)'                                        \
-    $(NULL)
-
-COMMON_SOURCES               = test_platform.cpp test_util.cpp
-
-# Source, compiler, and linker options for test programs.
-
-ot_test_aes_LDADD                   = $(COMMON_LDADD)
-ot_test_aes_LIBTOOLFLAGS            = $(COMMON_LIBTOOLFLAGS)
-ot_test_aes_SOURCES                 = $(COMMON_SOURCES) test_aes.cpp
-
-ot_test_array_LDADD                 = $(COMMON_LDADD)
-ot_test_array_LIBTOOLFLAGS          = $(COMMON_LIBTOOLFLAGS)
-ot_test_array_SOURCES               = $(COMMON_SOURCES) test_array.cpp
-
-ot_test_binary_search_LDADD         = $(COMMON_LDADD)
-ot_test_binary_search_LIBTOOLFLAGS  = $(COMMON_LIBTOOLFLAGS)
-ot_test_binary_search_SOURCES       = $(COMMON_SOURCES) test_binary_search.cpp
-
-ot_test_checksum_LDADD              = $(COMMON_LDADD)
-ot_test_checksum_LIBTOOLFLAGS       = $(COMMON_LIBTOOLFLAGS)
-ot_test_checksum_SOURCES            = $(COMMON_SOURCES) test_checksum.cpp
-
-ot_test_child_LDADD                 = $(COMMON_LDADD)
-ot_test_child_LIBTOOLFLAGS          = $(COMMON_LIBTOOLFLAGS)
-ot_test_child_SOURCES               = $(COMMON_SOURCES) test_child.cpp
-
-ot_test_child_table_LDADD           = $(COMMON_LDADD)
-ot_test_child_table_LIBTOOLFLAGS    = $(COMMON_LIBTOOLFLAGS)
-ot_test_child_table_SOURCES         = $(COMMON_SOURCES) test_child_table.cpp
-
-ot_test_cmd_line_parser_LDADD       = $(COMMON_LDADD)
-ot_test_cmd_line_parser_LIBTOOLFLAGS = $(COMMON_LIBTOOLFLAGS)
-ot_test_cmd_line_parser_SOURCES     = $(COMMON_SOURCES) test_cmd_line_parser.cpp
-
-ot_test_data_LDADD                  = $(COMMON_LDADD)
-ot_test_data_LIBTOOLFLAGS           = $(COMMON_LIBTOOLFLAGS)
-ot_test_data_SOURCES                = $(COMMON_SOURCES) test_data.cpp
-
-ot_test_dns_LDADD                   = $(COMMON_LDADD)
-ot_test_dns_LIBTOOLFLAGS            = $(COMMON_LIBTOOLFLAGS)
-ot_test_dns_SOURCES                 = $(COMMON_SOURCES) test_dns.cpp
-
-ot_test_dso_LDADD                   = $(COMMON_LDADD)
-ot_test_dso_LIBTOOLFLAGS            = $(COMMON_LIBTOOLFLAGS)
-ot_test_dso_SOURCES                 = $(COMMON_SOURCES) test_dso.cpp
-
-ot_test_ecdsa_LDADD                 = $(COMMON_LDADD)
-ot_test_ecdsa_LIBTOOLFLAGS          = $(COMMON_LIBTOOLFLAGS)
-ot_test_ecdsa_SOURCES               = $(COMMON_SOURCES) test_ecdsa.cpp
-
-ot_test_flash_LDADD                 = $(COMMON_LDADD)
-ot_test_flash_LIBTOOLFLAGS          = $(COMMON_LIBTOOLFLAGS)
-ot_test_flash_SOURCES               = $(COMMON_SOURCES) test_flash.cpp
-
-ot_test_frame_builder_LDADD         = $(COMMON_LDADD)
-ot_test_frame_builder_LIBTOOLFLAGS  = $(COMMON_LIBTOOLFLAGS)
-ot_test_frame_builder_SOURCES       = $(COMMON_SOURCES) test_frame_builder.cpp
-
-ot_test_hdlc_LDADD                  = $(COMMON_LDADD)
-ot_test_hdlc_LIBTOOLFLAGS           = $(COMMON_LIBTOOLFLAGS)
-ot_test_hdlc_SOURCES                = $(COMMON_SOURCES) test_hdlc.cpp
-
-ot_test_heap_LDADD                  = $(COMMON_LDADD)
-ot_test_heap_LIBTOOLFLAGS           = $(COMMON_LIBTOOLFLAGS)
-ot_test_heap_SOURCES                = $(COMMON_SOURCES) test_heap.cpp
-
-ot_test_heap_array_LDADD           = $(COMMON_LDADD)
-ot_test_heap_array_LIBTOOLFLAGS    = $(COMMON_LIBTOOLFLAGS)
-ot_test_heap_array_SOURCES         = $(COMMON_SOURCES) test_heap_array.cpp
-
-ot_test_heap_string_LDADD           = $(COMMON_LDADD)
-ot_test_heap_string_LIBTOOLFLAGS    = $(COMMON_LIBTOOLFLAGS)
-ot_test_heap_string_SOURCES         = $(COMMON_SOURCES) test_heap_string.cpp
-
-ot_test_hkdf_sha256_LDADD           = $(COMMON_LDADD)
-ot_test_hkdf_sha256_LIBTOOLFLAGS    = $(COMMON_LIBTOOLFLAGS)
-ot_test_hkdf_sha256_SOURCES         = $(COMMON_SOURCES) test_hkdf_sha256.cpp
-
-ot_test_hmac_sha256_LDADD           = $(COMMON_LDADD)
-ot_test_hmac_sha256_LIBTOOLFLAGS    = $(COMMON_LIBTOOLFLAGS)
-ot_test_hmac_sha256_SOURCES         = $(COMMON_SOURCES) test_hmac_sha256.cpp
-
-ot_test_ip4_header_LDADD            = $(COMMON_LDADD)
-ot_test_ip4_header_LIBTOOLFLAGS     = $(COMMON_LIBTOOLFLAGS)
-ot_test_ip4_header_SOURCES          = $(COMMON_SOURCES) test_ip4_header.cpp
-
-ot_test_ip6_header_LDADD            = $(COMMON_LDADD)
-ot_test_ip6_header_LIBTOOLFLAGS     = $(COMMON_LIBTOOLFLAGS)
-ot_test_ip6_header_SOURCES          = $(COMMON_SOURCES) test_ip6_header.cpp
-
-ot_test_ip_address_LDADD            = $(COMMON_LDADD)
-ot_test_ip_address_LIBTOOLFLAGS     = $(COMMON_LIBTOOLFLAGS)
-ot_test_ip_address_SOURCES          = $(COMMON_SOURCES) test_ip_address.cpp
-
-ot_test_link_quality_LDADD          = $(COMMON_LDADD)
-ot_test_link_quality_LIBTOOLFLAGS   = $(COMMON_LIBTOOLFLAGS)
-ot_test_link_quality_SOURCES        = $(COMMON_SOURCES) test_link_quality.cpp
-
-ot_test_linked_list_LDADD           = $(COMMON_LDADD)
-ot_test_linked_list_LIBTOOLFLAGS    = $(COMMON_LIBTOOLFLAGS)
-ot_test_linked_list_SOURCES         = $(COMMON_SOURCES) test_linked_list.cpp
-
-ot_test_lowpan_LDADD                = $(COMMON_LDADD)
-ot_test_lowpan_LIBTOOLFLAGS         = $(COMMON_LIBTOOLFLAGS)
-ot_test_lowpan_SOURCES              = $(COMMON_SOURCES) test_lowpan.cpp
-
-ot_test_mac_frame_LDADD             = $(COMMON_LDADD)
-ot_test_mac_frame_LIBTOOLFLAGS      = $(COMMON_LIBTOOLFLAGS)
-ot_test_mac_frame_SOURCES           = $(COMMON_SOURCES) test_mac_frame.cpp
-
-ot_test_macros_LDADD                = $(COMMON_LDADD)
-ot_test_macros_LIBTOOLFLAGS         = $(COMMON_LIBTOOLFLAGS)
-ot_test_macros_SOURCES              = $(COMMON_SOURCES) test_macros.cpp
-
-ot_test_message_LDADD               = $(COMMON_LDADD)
-ot_test_message_LIBTOOLFLAGS        = $(COMMON_LIBTOOLFLAGS)
-ot_test_message_SOURCES             = $(COMMON_SOURCES) test_message.cpp
-
-ot_test_message_queue_LDADD         = $(COMMON_LDADD)
-ot_test_message_queue_LIBTOOLFLAGS  = $(COMMON_LIBTOOLFLAGS)
-ot_test_message_queue_SOURCES       = $(COMMON_SOURCES) test_message_queue.cpp
-
-ot_test_multicast_listeners_table_LDADD = $(COMMON_LDADD)
-ot_test_multicast_listeners_table_LIBTOOLFLAGS = $(COMMON_LIBTOOLFLAGS)
-ot_test_multicast_listeners_table_SOURCES = $(COMMON_SOURCES) test_multicast_listeners_table.cpp
-
-ot_test_network_name_LDADD          = $(COMMON_LDADD)
-ot_test_network_name_LIBTOOLFLAGS   = $(COMMON_LIBTOOLFLAGS)
-ot_test_network_name_SOURCES        = $(COMMON_SOURCES) test_network_name.cpp
-
-ot_test_spinel_buffer_LDADD         = $(COMMON_LDADD)
-ot_test_spinel_buffer_LIBTOOLFLAGS  = $(COMMON_LIBTOOLFLAGS)
-ot_test_spinel_buffer_SOURCES       = $(COMMON_SOURCES) test_spinel_buffer.cpp
-
-ot_test_ndproxy_table_LDADD         = $(COMMON_LDADD)
-ot_test_ndproxy_table_LIBTOOLFLAGS  = $(COMMON_LIBTOOLFLAGS)
-ot_test_ndproxy_table_SOURCES       = $(COMMON_SOURCES) test_ndproxy_table.cpp
-
-ot_test_netif_LDADD                 = $(COMMON_LDADD)
-ot_test_netif_LIBTOOLFLAGS          = $(COMMON_LIBTOOLFLAGS)
-ot_test_netif_SOURCES               = $(COMMON_SOURCES) test_netif.cpp
-
-ot_test_network_data_LDADD          = $(COMMON_LDADD)
-ot_test_network_data_LIBTOOLFLAGS    = $(COMMON_LIBTOOLFLAGS)
-ot_test_network_data_SOURCES        = $(COMMON_SOURCES) test_network_data.cpp
-
-ot_test_pool_LDADD                  = $(COMMON_LDADD)
-ot_test_pool_LIBTOOLFLAGS           = $(COMMON_LIBTOOLFLAGS)
-ot_test_pool_SOURCES                = $(COMMON_SOURCES) test_pool.cpp
-
-ot_test_priority_queue_LDADD        = $(COMMON_LDADD)
-ot_test_priority_queue_LIBTOOLFLAGS = $(COMMON_LIBTOOLFLAGS)
-ot_test_priority_queue_SOURCES      = $(COMMON_SOURCES) test_priority_queue.cpp
-
-ot_test_pskc_LDADD                  = $(COMMON_LDADD)
-ot_test_pskc_LIBTOOLFLAGS           = $(COMMON_LIBTOOLFLAGS)
-ot_test_pskc_SOURCES                = $(COMMON_SOURCES) test_pskc.cpp
-
-ot_test_smart_ptrs_LDADD            = $(COMMON_LDADD)
-ot_test_smart_ptrs_LIBTOOLFLAGS     = $(COMMON_LIBTOOLFLAGS)
-ot_test_smart_ptrs_SOURCES          = $(COMMON_SOURCES) test_smart_ptrs.cpp
-
-ot_test_meshcop_LDADD               = $(COMMON_LDADD)
-ot_test_meshcop_LIBTOOLFLAGS        = $(COMMON_LIBTOOLFLAGS)
-ot_test_meshcop_SOURCES             = $(COMMON_SOURCES) test_meshcop.cpp
-
-ot_test_routing_manager_LDADD        = $(COMMON_LDADD)
-ot_test_routing_manager_LIBTOOLFLAGS = $(COMMON_LIBTOOLFLAGS)
-ot_test_routing_manager_SOURCES      = $(COMMON_SOURCES) test_routing_manager.cpp
-
-ot_test_serial_number_LDADD         = $(COMMON_LDADD)
-ot_test_serial_number_LIBTOOLFLAGS  = $(COMMON_LIBTOOLFLAGS)
-ot_test_serial_number_SOURCES       = $(COMMON_SOURCES) test_serial_number.cpp
-
-ot_test_string_LDADD                = $(COMMON_LDADD)
-ot_test_string_LIBTOOLFLAGS         = $(COMMON_LIBTOOLFLAGS)
-ot_test_string_SOURCES              = $(COMMON_SOURCES) test_string.cpp
-
-ot_test_spinel_decoder_LDADD        = $(COMMON_LDADD)
-ot_test_spinel_decoder_LIBTOOLFLAGS = $(COMMON_LIBTOOLFLAGS)
-ot_test_spinel_decoder_SOURCES      = $(COMMON_SOURCES) test_spinel_decoder.cpp
-
-ot_test_spinel_encoder_LDADD        = $(COMMON_LDADD)
-ot_test_spinel_encoder_LIBTOOLFLAGS = $(COMMON_LIBTOOLFLAGS)
-ot_test_spinel_encoder_SOURCES      = $(COMMON_SOURCES) test_spinel_encoder.cpp
-
-ot_test_timer_LDADD                 = $(COMMON_LDADD)
-ot_test_timer_LIBTOOLFLAGS          = $(COMMON_LIBTOOLFLAGS)
-ot_test_timer_SOURCES               = $(COMMON_SOURCES) test_timer.cpp
-
-ot_test_toolchain_LDADD             = $(NULL)
-ot_test_toolchain_SOURCES           = test_toolchain.cpp test_toolchain_c.c
-
-if OPENTHREAD_BUILD_COVERAGE
-CLEANFILES                   = $(wildcard *.gcda *.gcno)
-endif # OPENTHREAD_BUILD_COVERAGE
-
-endif # OPENTHREAD_BUILD_TESTS
-
-include $(abs_top_nlbuild_autotools_dir)/automake/post.am
diff --git a/tests/unit/README.md b/tests/unit/README.md
new file mode 100644
index 0000000..132062c
--- /dev/null
+++ b/tests/unit/README.md
@@ -0,0 +1,57 @@
+# OpenThread Unit Tests
+
+This page describes how to build and run OpenThread unit tests. It will be helpful for developers to debug failed unit test cases if they got one in CI or to add some new test cases.
+
+## Build Simulation
+
+The unit tests cannot be built solely without building the whole project. So first build OpenThread on the simulation platform, which will also build all unit tests:
+
+```
+# Go to the root directory of OpenThread
+$ script/cmake-build simulation
+```
+
+## List all tests
+
+To see what tests are available in OpenThread:
+
+```
+# Make sure you are at the simulation build directory (build/simulation)
+$ ctest -N
+```
+
+## Run the Unit Tests
+
+To run all the unit tests:
+
+```
+# Make sure you are at the simulation build directory (build/simulation)
+$ ctest
+```
+
+To run a specific unit test, for example, `ot-test-spinel`:
+
+```
+# Make sure you are at the simulation build directory (build/simulation)
+$ ctest -R ot-test-spinel
+```
+
+## Update a Test Case
+
+If you are developing a unit test case and have made some changes in the test source file, you will need rebuild the test before running it:
+
+```
+# Make sure you are at the simulation build directory (build/simulation)
+$ ninja <test_name>
+```
+
+This will only build the test and take a short time.
+
+If any changes or fixes were made to the OpenThread code, then you'll need to rebuild the entire project:
+
+```
+# Make sure you are at the simulation build directory (build/simulation)
+$ ninja
+```
+
+This will build the updated OpenThread code as well as the test cases.
diff --git a/tests/unit/test_aes.cpp b/tests/unit/test_aes.cpp
index ce75b28..e88b1e2 100644
--- a/tests/unit/test_aes.cpp
+++ b/tests/unit/test_aes.cpp
@@ -55,7 +55,7 @@
                            0xAC, 0x02, 0x05, 0x00, 0x00, 0x00, 0x55, 0xCF, 0x00, 0x00, 0x51, 0x52,
                            0x53, 0x54, 0x22, 0x3B, 0xC1, 0xEC, 0x84, 0x1A, 0xB5, 0x53};
 
-    otInstance *       instance = testInitInstance();
+    otInstance        *instance = testInitInstance();
     ot::Crypto::AesCcm aesCcm;
     uint32_t           headerLength  = sizeof(test) - 8;
     uint32_t           payloadLength = 0;
@@ -122,8 +122,8 @@
 
     uint8_t tag[kTagLength];
 
-    ot::Instance *     instance = testInitInstance();
-    ot::Message *      message;
+    ot::Instance      *instance = testInitInstance();
+    ot::Message       *message;
     ot::Crypto::AesCcm aesCcm;
 
     VerifyOrQuit(instance != nullptr);
@@ -195,9 +195,9 @@
     uint8_t header[kHeaderLength];
 
     ot::Crypto::AesCcm aesCcm;
-    ot::Instance *     instance = testInitInstance();
-    ot::Message *      message;
-    ot::Message *      messageClone;
+    ot::Instance      *instance = testInitInstance();
+    ot::Message       *message;
+    ot::Message       *messageClone;
 
     VerifyOrQuit(instance != nullptr);
 
diff --git a/tests/unit/test_binary_search.cpp b/tests/unit/test_binary_search.cpp
index bf05d9f..d2d0224 100644
--- a/tests/unit/test_binary_search.cpp
+++ b/tests/unit/test_binary_search.cpp
@@ -64,9 +64,12 @@
     constexpr Entry kUnsortedTable[]       = {{"z", 0}, {"a", 0}, {"b", 0}};
     constexpr Entry kDuplicateEntryTable[] = {{"duplicate", 1}, {"duplicate", 2}};
 
+// gcc-4 does not support constexpr function
+#if __GNUC__ > 4
     static_assert(BinarySearch::IsSorted(kTable), "IsSorted() failed");
     static_assert(!BinarySearch::IsSorted(kUnsortedTable), "failed for unsorted table");
     static_assert(!BinarySearch::IsSorted(kDuplicateEntryTable), "failed for table with duplicate entries");
+#endif
 
     for (const Entry &tableEntry : kTable)
     {
diff --git a/tests/unit/test_checksum.cpp b/tests/unit/test_checksum.cpp
index 101b885..b0b17b5 100644
--- a/tests/unit/test_checksum.cpp
+++ b/tests/unit/test_checksum.cpp
@@ -73,7 +73,7 @@
 uint16_t CalculateChecksum(const Ip6::Address &aSource,
                            const Ip6::Address &aDestination,
                            uint8_t             aIpProto,
-                           const Message &     aMessage)
+                           const Message      &aMessage)
 {
     // This method calculates the checksum over an IPv6 message.
     constexpr uint16_t kMaxPayload = 1024;
@@ -112,7 +112,7 @@
 uint16_t CalculateChecksum(const Ip4::Address &aSource,
                            const Ip4::Address &aDestination,
                            uint8_t             aIpProto,
-                           const Message &     aMessage)
+                           const Message      &aMessage)
 {
     // This method calculates the checksum over an IPv4 message.
     constexpr uint16_t kMaxPayload = 1024;
@@ -181,7 +181,7 @@
 
     for (uint16_t size = kMinSize; size <= kMaxSize; size++)
     {
-        Message *        message = instance->Get<Ip6::Ip6>().NewMessage(sizeof(Ip6::Udp::Header));
+        Message         *message = instance->Get<Ip6::Ip6>().NewMessage(sizeof(Ip6::Udp::Header));
         Ip6::Udp::Header udpHeader;
         Ip6::MessageInfo messageInfo;
 
@@ -249,7 +249,7 @@
 
     for (uint16_t size = kMinSize; size <= kMaxSize; size++)
     {
-        Message *         message = instance->Get<Ip6::Ip6>().NewMessage(sizeof(Ip6::Icmp::Header));
+        Message          *message = instance->Get<Ip6::Ip6>().NewMessage(sizeof(Ip6::Icmp::Header));
         Ip6::Icmp::Header icmp6Header;
         Ip6::MessageInfo  messageInfo;
 
@@ -324,7 +324,7 @@
 
     for (uint16_t size = kMinSize; size <= kMaxSize; size++)
     {
-        Message *        message = instance->Get<Ip6::Ip6>().NewMessage(sizeof(Ip4::Tcp::Header));
+        Message         *message = instance->Get<Ip6::Ip6>().NewMessage(sizeof(Ip4::Tcp::Header));
         Ip4::Tcp::Header tcpHeader;
 
         VerifyOrQuit(message != nullptr, "Ip6::NewMesssage() failed");
@@ -379,7 +379,7 @@
 
     for (uint16_t size = kMinSize; size <= kMaxSize; size++)
     {
-        Message *        message = instance->Get<Ip6::Ip6>().NewMessage(sizeof(Ip4::Udp::Header));
+        Message         *message = instance->Get<Ip6::Ip6>().NewMessage(sizeof(Ip4::Udp::Header));
         Ip4::Udp::Header udpHeader;
 
         VerifyOrQuit(message != nullptr, "Ip6::NewMesssage() failed");
@@ -418,13 +418,13 @@
 void TestIcmp4MessageChecksum(void)
 {
     // A captured ICMP echo request (ping) message. Checksum field is set to zero.
-    const uint8_t kExampleIcmpMessage[] = "\x08\x00\x00\x00\x67\x2e\x00\x00\x62\xaf\xf1\x61\x00\x04\xfc\x24"
-                                          "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17"
-                                          "\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27"
-                                          "\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37";
-    uint16_t  kChecksumForExampleMessage = 0x5594;
-    Instance *instance                   = static_cast<Instance *>(testInitInstance());
-    Message * message                    = instance->Get<Ip6::Ip6>().NewMessage(sizeof(kExampleIcmpMessage));
+    const uint8_t kExampleIcmpMessage[]      = "\x08\x00\x00\x00\x67\x2e\x00\x00\x62\xaf\xf1\x61\x00\x04\xfc\x24"
+                                               "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17"
+                                               "\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27"
+                                               "\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37";
+    uint16_t      kChecksumForExampleMessage = 0x5594;
+    Instance     *instance                   = static_cast<Instance *>(testInitInstance());
+    Message      *message                    = instance->Get<Ip6::Ip6>().NewMessage(sizeof(kExampleIcmpMessage));
 
     Ip4::Address source;
     Ip4::Address dest;
diff --git a/tests/unit/test_child.cpp b/tests/unit/test_child.cpp
index fb30ace..2a796a3 100644
--- a/tests/unit/test_child.cpp
+++ b/tests/unit/test_child.cpp
@@ -191,10 +191,10 @@
     Child        child;
     Ip6::Address addresses[kMaxChildIp6Addresses];
     uint8_t      numAddresses;
-    const char * ip6Addresses[] = {
-        "fd00:1234::1234",
-        "ff6b:e251:52fb:0:12e6:b94c:1c28:c56a",
-        "fd00:1234::204c:3d7c:98f6:9a1b",
+    const char  *ip6Addresses[] = {
+         "fd00:1234::1234",
+         "ff6b:e251:52fb:0:12e6:b94c:1c28:c56a",
+         "fd00:1234::204c:3d7c:98f6:9a1b",
     };
 
     const uint8_t            meshLocalIidArray[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
diff --git a/tests/unit/test_child_table.cpp b/tests/unit/test_child_table.cpp
index 3281baa..2fb4583 100644
--- a/tests/unit/test_child_table.cpp
+++ b/tests/unit/test_child_table.cpp
@@ -124,7 +124,7 @@
 
         for (uint16_t listIndex = 0; listIndex < aChildListLength; listIndex++)
         {
-            Child *      child;
+            Child       *child;
             Mac::Address address;
 
             if (!StateMatchesFilter(aChildList[listIndex].mState, filter))
@@ -164,8 +164,8 @@
 
             for (; !iter.IsDone(); iter++)
             {
-                Child *  child    = iter.GetChild();
-                Child &  childRef = *iter;
+                Child   *child    = iter.GetChild();
+                Child   &childRef = *iter;
                 bool     didFind  = false;
                 uint16_t childIndex;
 
diff --git a/tests/unit/test_cmd_line_parser.cpp b/tests/unit/test_cmd_line_parser.cpp
index 46d5915..7f832cf 100644
--- a/tests/unit/test_cmd_line_parser.cpp
+++ b/tests/unit/test_cmd_line_parser.cpp
@@ -252,7 +252,7 @@
     uint8_t        buffer[sizeof(kEvenParsedArray)];
     uint8_t        buf3[3];
     uint16_t       len;
-    const char *   string;
+    const char    *string;
     const uint8_t *bufPtr;
 
     // Verify `ParseAsHexString(const char *aString, uint8_t *aBuffer, uint16_t aSize)`
diff --git a/tests/unit/test_dns.cpp b/tests/unit/test_dns.cpp
index 074cb9c..6df6b3b 100644
--- a/tests/unit/test_dns.cpp
+++ b/tests/unit/test_dns.cpp
@@ -49,24 +49,25 @@
 
     struct TestName
     {
-        const char *   mName;
+        const char    *mName;
         uint16_t       mEncodedLength;
         const uint8_t *mEncodedData;
-        const char **  mLabels;
-        const char *   mExpectedReadName;
+        const char   **mLabels;
+        const char    *mExpectedReadName;
     };
 
-    Instance *   instance;
+    Instance    *instance;
     MessagePool *messagePool;
-    Message *    message;
+    Message     *message;
     uint8_t      buffer[kMaxSize];
     uint16_t     len;
     uint16_t     offset;
     char         label[Dns::Name::kMaxLabelSize];
     uint8_t      labelLength;
     char         name[Dns::Name::kMaxNameSize];
-    const char * subDomain;
-    const char * domain;
+    const char  *subDomain;
+    const char  *domain;
+    const char  *domain2;
 
     static const uint8_t kEncodedName1[] = {7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0};
     static const uint8_t kEncodedName2[] = {3, 'f', 'o', 'o', 1, 'a', 2, 'b', 'b', 3, 'e', 'd', 'u', 0};
@@ -210,6 +211,42 @@
     domain    = "Vice.arpa.";
     VerifyOrQuit(!Dns::Name::IsSubDomainOf(subDomain, domain));
 
+    domain  = "example.com.";
+    domain2 = "example.com.";
+    VerifyOrQuit(Dns::Name::IsSameDomain(domain, domain2));
+
+    domain  = "example.com.";
+    domain2 = "example.com";
+    VerifyOrQuit(Dns::Name::IsSameDomain(domain, domain2));
+
+    domain  = "example.com.";
+    domain2 = "ExAmPlE.cOm";
+    VerifyOrQuit(Dns::Name::IsSameDomain(domain, domain2));
+
+    domain  = "example.com";
+    domain2 = "ExAmPlE.cOm";
+    VerifyOrQuit(Dns::Name::IsSameDomain(domain, domain2));
+
+    domain  = "example.com.";
+    domain2 = "ExAmPlE.cOm.";
+    VerifyOrQuit(Dns::Name::IsSameDomain(domain, domain2));
+
+    domain  = "example.com.";
+    domain2 = "aExAmPlE.cOm.";
+    VerifyOrQuit(!Dns::Name::IsSameDomain(domain, domain2));
+
+    domain  = "example.com.";
+    domain2 = "cOm.";
+    VerifyOrQuit(!Dns::Name::IsSameDomain(domain, domain2));
+
+    domain  = "example.";
+    domain2 = "example.com.";
+    VerifyOrQuit(!Dns::Name::IsSameDomain(domain, domain2));
+
+    domain  = "example.com.";
+    domain2 = ".example.com.";
+    VerifyOrQuit(!Dns::Name::IsSameDomain(domain, domain2));
+
     printf("----------------------------------------------------------------\n");
     printf("Append names, check encoded bytes, parse name and read labels:\n");
 
@@ -444,10 +481,10 @@
 
     static const char kBadName[] = "bad.name";
 
-    Instance *   instance;
+    Instance    *instance;
     MessagePool *messagePool;
-    Message *    message;
-    Message *    message2;
+    Message     *message;
+    Message     *message2;
     uint16_t     offset;
     uint16_t     name1Offset;
     uint16_t     name2Offset;
@@ -799,9 +836,9 @@
     const char *kInstanceLabels[] = {kInstance1Label, kInstance2Label};
     const char *kInstanceNames[]  = {kInstance1Name, kInstance2Name};
 
-    Instance *          instance;
-    MessagePool *       messagePool;
-    Message *           message;
+    Instance           *instance;
+    MessagePool        *messagePool;
+    Message            *message;
     Dns::Header         header;
     uint16_t            messageId;
     uint16_t            headerOffset;
@@ -1021,6 +1058,8 @@
 
     for (const char *instanceName : kInstanceNames)
     {
+        uint16_t savedOffset;
+
         // SRV record
         SuccessOrQuit(Dns::Name::CompareName(*message, offset, instanceName));
         SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, srvRecord));
@@ -1037,12 +1076,21 @@
         SuccessOrQuit(Dns::Name::CompareName(*message, offset, instanceName));
         SuccessOrQuit(Dns::ResourceRecord::ReadRecord(*message, offset, txtRecord));
         VerifyOrQuit(txtRecord.GetTtl() == kTxtTtl);
-        len = sizeof(buffer);
+        savedOffset = offset;
+        len         = sizeof(buffer);
         SuccessOrQuit(txtRecord.ReadTxtData(*message, offset, buffer, len));
         VerifyOrQuit(len == sizeof(kTxtData));
         VerifyOrQuit(memcmp(buffer, kTxtData, len) == 0);
         printf("    \"%s\" TXT %u %d \"%s\"\n", instanceName, txtRecord.GetTtl(), txtRecord.GetLength(),
                reinterpret_cast<const char *>(buffer));
+
+        // Partial read of TXT data
+        len = sizeof(kTxtData) - 1;
+        memset(buffer, 0, sizeof(buffer));
+        VerifyOrQuit(txtRecord.ReadTxtData(*message, savedOffset, buffer, len) == kErrorNoBufs);
+        VerifyOrQuit(len == sizeof(kTxtData) - 1);
+        VerifyOrQuit(memcmp(buffer, kTxtData, len) == 0);
+        VerifyOrQuit(savedOffset == offset);
     }
 
     SuccessOrQuit(Dns::Name::CompareName(*message, offset, kHostName));
@@ -1229,9 +1277,9 @@
         {kEncodedTxt5, sizeof(kEncodedTxt5)}, {kEncodedTxt6, sizeof(kEncodedTxt6)},
         {kEncodedTxt7, sizeof(kEncodedTxt7)}};
 
-    Instance *                     instance;
-    MessagePool *                  messagePool;
-    Message *                      message;
+    Instance                      *instance;
+    MessagePool                   *messagePool;
+    Message                       *message;
     uint8_t                        txtData[kMaxTxtDataSize];
     uint16_t                       txtDataLength;
     uint8_t                        index;
diff --git a/tests/unit/test_dns_client.cpp b/tests/unit/test_dns_client.cpp
new file mode 100644
index 0000000..1b27a59
--- /dev/null
+++ b/tests/unit/test_dns_client.cpp
@@ -0,0 +1,682 @@
+/*
+ *  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;
+}
+
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+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[256];
+};
+
+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));
+    LogServiceInfo(sResolveServiceInfo.mInfo);
+
+exit:
+    return;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+
+void TestDnsClient(void)
+{
+    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,
+    };
+
+    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();
+
+    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 unknwon 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);
+        }
+
+        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);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
+
+    Log("Stop DNS-SD server");
+    dnsServer->Stop();
+
+    Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName,
+        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrv));
+
+    queryConfig.Clear();
+    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrv);
+
+    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_dso.cpp b/tests/unit/test_dso.cpp
index 7b878e8..2ee1828 100644
--- a/tests/unit/test_dso.cpp
+++ b/tests/unit/test_dso.cpp
@@ -35,6 +35,7 @@
 #include "common/array.hpp"
 #include "common/as_core_type.hpp"
 #include "common/instance.hpp"
+#include "common/time.hpp"
 #include "net/dns_dso.hpp"
 
 #if OPENTHREAD_CONFIG_DNS_DSO_ENABLE
@@ -51,10 +52,7 @@
     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__))
 
-void otPlatAlarmMilliStop(otInstance *)
-{
-    sAlarmOn = false;
-}
+void otPlatAlarmMilliStop(otInstance *) { sAlarmOn = false; }
 
 void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt)
 {
@@ -65,10 +63,7 @@
         (sAlarmTime - sNow) / 1000, (sAlarmTime - sNow) % 1000);
 }
 
-uint32_t otPlatAlarmMilliGetNow(void)
-{
-    return sNow;
-}
+uint32_t otPlatAlarmMilliGetNow(void) { return sNow; }
 
 } // extern "C"
 
@@ -78,7 +73,7 @@
 
     Log(" AdvanceTime for %u.%03u", aDuration / 1000, aDuration % 1000);
 
-    while (sAlarmTime <= time)
+    while (ot::TimeMilli(sAlarmTime) <= ot::TimeMilli(time))
     {
         sNow = sAlarmTime;
         otPlatAlarmMilliFired(sInstance);
@@ -117,8 +112,8 @@
     friend void otPlatDsoSend(otPlatDsoConnection *aConnection, otMessage *aMessage);
 
 public:
-    explicit Connection(Instance &           aInstance,
-                        const char *         aName,
+    explicit Connection(Instance            &aInstance,
+                        const char          *aName,
                         const Ip6::SockAddr &aLocalSockAddr,
                         const Ip6::SockAddr &aPeerSockAddr)
         : Dso::Connection(aInstance, aPeerSockAddr, sCallbacks)
@@ -128,7 +123,7 @@
         ClearTestFlags();
     }
 
-    const char *         GetName(void) const { return mName; }
+    const char          *GetName(void) const { return mName; }
     const Ip6::SockAddr &GetLocalSockAddr(void) const { return mLocalSockAddr; }
 
     void ClearTestFlags(void)
@@ -246,7 +241,7 @@
     }
 
     Error ProcessResponseMessage(const Dns::Header &aHeader,
-                                 const Message &    aMessage,
+                                 const Message     &aMessage,
                                  Dso::Tlv::Type     aResponseTlvType,
                                  Dso::Tlv::Type     aRequestTlvType)
     {
@@ -288,22 +283,22 @@
 
     static Error ProcessRequestMessage(Dso::Connection &aConnection,
                                        MessageId        aMessageId,
-                                       const Message &  aMessage,
+                                       const Message   &aMessage,
                                        Dso::Tlv::Type   aPrimaryTlvType)
     {
         return static_cast<Connection &>(aConnection).ProcessRequestMessage(aMessageId, aMessage, aPrimaryTlvType);
     }
 
     static Error ProcessUnidirectionalMessage(Dso::Connection &aConnection,
-                                              const Message &  aMessage,
+                                              const Message   &aMessage,
                                               Dso::Tlv::Type   aPrimaryTlvType)
     {
         return static_cast<Connection &>(aConnection).ProcessUnidirectionalMessage(aMessage, aPrimaryTlvType);
     }
 
-    static Error ProcessResponseMessage(Dso::Connection &  aConnection,
+    static Error ProcessResponseMessage(Dso::Connection   &aConnection,
                                         const Dns::Header &aHeader,
-                                        const Message &    aMessage,
+                                        const Message     &aMessage,
                                         Dso::Tlv::Type     aResponseTlvType,
                                         Dso::Tlv::Type     aRequestTlvType)
     {
@@ -311,7 +306,7 @@
             .ProcessResponseMessage(aHeader, aMessage, aResponseTlvType, aRequestTlvType);
     }
 
-    const char *          mName;
+    const char           *mName;
     Ip6::SockAddr         mLocalSockAddr;
     bool                  mDidGetConnectedSignal;
     bool                  mDidGetSessionEstablishedSignal;
@@ -379,8 +374,8 @@
 
 void otPlatDsoConnect(otPlatDsoConnection *aConnection, const otSockAddr *aPeerSockAddr)
 {
-    Connection &         conn         = *static_cast<Connection *>(aConnection);
-    Connection *         peerConn     = nullptr;
+    Connection          &conn         = *static_cast<Connection *>(aConnection);
+    Connection          *peerConn     = nullptr;
     const Ip6::SockAddr &peerSockAddr = AsCoreType(aPeerSockAddr);
 
     Log(" otPlatDsoConnect(%s, aPeer:0x%04x)", conn.GetName(), peerSockAddr.GetPort());
@@ -544,12 +539,12 @@
     static constexpr uint32_t kRetryDelayInterval  = TimeMilli::SecToMsec(3600);
     static constexpr uint32_t kLongResponseTimeout = Dso::kResponseTimeout + TimeMilli::SecToMsec(17);
 
-    Instance &            instance = *static_cast<Instance *>(testInitInstance());
+    Instance             &instance = *static_cast<Instance *>(testInitInstance());
     Ip6::SockAddr         serverSockAddr(kPortA);
     Ip6::SockAddr         clientSockAddr(kPortB);
     Connection            serverConn(instance, "serverConn", serverSockAddr, clientSockAddr);
     Connection            clientConn(instance, "clinetConn", clientSockAddr, serverSockAddr);
-    Message *             message;
+    Message              *message;
     Dso::Tlv              tlv;
     Connection::MessageId messageId;
 
diff --git a/tests/unit/test_ecdsa.cpp b/tests/unit/test_ecdsa.cpp
index 533ebcc..aad13a7 100644
--- a/tests/unit/test_ecdsa.cpp
+++ b/tests/unit/test_ecdsa.cpp
@@ -66,12 +66,14 @@
 
     const uint8_t kMessage[] = {'s', 'a', 'm', 'p', 'l', 'e'};
 
+#if OPENTHREAD_CONFIG_DETERMINISTIC_ECDSA_ENABLE
     const uint8_t kExpectedSignature[] = {
         0xEF, 0xD4, 0x8B, 0x2A, 0xAC, 0xB6, 0xA8, 0xFD, 0x11, 0x40, 0xDD, 0x9C, 0xD4, 0x5E, 0x81, 0xD6,
         0x9D, 0x2C, 0x87, 0x7B, 0x56, 0xAA, 0xF9, 0x91, 0xC3, 0x4D, 0x0E, 0xA8, 0x4E, 0xAF, 0x37, 0x16,
         0xF7, 0xCB, 0x1C, 0x94, 0x2D, 0x65, 0x7C, 0x41, 0xD4, 0x36, 0xC7, 0xA1, 0xB6, 0xE2, 0x9F, 0x65,
         0xF3, 0xE9, 0x00, 0xDB, 0xB9, 0xAF, 0xF4, 0x06, 0x4D, 0xC4, 0xAB, 0x2F, 0x84, 0x3A, 0xCD, 0xA8,
     };
+#endif
 
     Instance *instance = testInitInstance();
 
@@ -110,6 +112,7 @@
     SuccessOrQuit(keyPair.Sign(hash, signature));
     DumpBuffer("Signature", signature.GetBytes(), sizeof(signature));
 
+#if OPENTHREAD_CONFIG_DETERMINISTIC_ECDSA_ENABLE
     printf("\nCheck signature against expected sequence----------------------------------\n");
     DumpBuffer("Expected signature", kExpectedSignature, sizeof(kExpectedSignature));
 
@@ -117,6 +120,7 @@
     VerifyOrQuit(memcmp(signature.GetBytes(), kExpectedSignature, sizeof(kExpectedSignature)) == 0);
 
     printf("Signature matches expected sequence.\n");
+#endif
 
     printf("\nVerify the signature ------------------------------------------------------\n");
     SuccessOrQuit(publicKey.Verify(hash, signature));
@@ -125,7 +129,7 @@
     testFreeInstance(instance);
 }
 
-void TestEdsaKeyGenerationSignAndVerify(void)
+void TestEcdsaKeyGenerationSignAndVerify(void)
 {
     Instance *instance = testInitInstance();
 
@@ -183,7 +187,7 @@
 {
 #if OPENTHREAD_CONFIG_ECDSA_ENABLE
     ot::Crypto::TestEcdsaVector();
-    ot::Crypto::TestEdsaKeyGenerationSignAndVerify();
+    ot::Crypto::TestEcdsaKeyGenerationSignAndVerify();
     printf("All tests passed\n");
 #else
     printf("ECDSA feature is not enabled\n");
diff --git a/tests/unit/test_frame_builder.cpp b/tests/unit/test_frame_builder.cpp
index cbc09c0..8c648e3 100644
--- a/tests/unit/test_frame_builder.cpp
+++ b/tests/unit/test_frame_builder.cpp
@@ -39,11 +39,12 @@
 {
     const uint8_t kData1[] = {0x01, 0x02, 0x03, 0x04, 0x05};
     const uint8_t kData2[] = {0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa};
+    const uint8_t kData3[] = {0xca, 0xfe, 0xbe, 0xef};
 
     static constexpr uint16_t kMaxBufferSize = sizeof(kData1) * 2 + sizeof(kData2);
 
-    Instance *   instance;
-    Message *    message;
+    Instance    *instance;
+    Message     *message;
     uint16_t     offset;
     uint8_t      buffer[kMaxBufferSize];
     uint8_t      zeroBuffer[kMaxBufferSize];
@@ -72,12 +73,25 @@
     VerifyOrQuit(frameBuilder.CanAppend(sizeof(buffer)));
     VerifyOrQuit(!frameBuilder.CanAppend(sizeof(buffer) + 1));
 
+    frameBuilder.SetMaxLength(sizeof(kData1));
+    VerifyOrQuit(frameBuilder.GetBytes() == buffer);
+    VerifyOrQuit(frameBuilder.GetLength() == 0);
+    VerifyOrQuit(frameBuilder.GetMaxLength() == sizeof(kData1));
+    VerifyOrQuit(memcmp(buffer, zeroBuffer, sizeof(buffer)) == 0);
+    VerifyOrQuit(frameBuilder.CanAppend(sizeof(kData1)));
+    VerifyOrQuit(!frameBuilder.CanAppend(sizeof(kData1) + 1));
+
     SuccessOrQuit(frameBuilder.Append(kData1));
     VerifyOrQuit(frameBuilder.GetLength() == sizeof(kData1));
     VerifyOrQuit(frameBuilder.GetBytes() == buffer);
     VerifyOrQuit(memcmp(buffer, kData1, sizeof(kData1)) == 0);
     VerifyOrQuit(memcmp(buffer + sizeof(kData1), zeroBuffer, sizeof(buffer) - sizeof(kData1)) == 0);
 
+    frameBuilder.SetMaxLength(sizeof(buffer));
+    VerifyOrQuit(frameBuilder.GetMaxLength() == sizeof(buffer));
+    VerifyOrQuit(frameBuilder.CanAppend(sizeof(buffer) - sizeof(kData1)));
+    VerifyOrQuit(!frameBuilder.CanAppend(sizeof(buffer) - sizeof(kData1) + 1));
+
     SuccessOrQuit(frameBuilder.AppendUint8(0x01));
     SuccessOrQuit(frameBuilder.AppendBigEndianUint16(0x0203));
     SuccessOrQuit(frameBuilder.AppendLittleEndianUint16(0x0504));
@@ -135,6 +149,66 @@
     VerifyOrQuit(memcmp(buffer + sizeof(kData1), kData2, sizeof(kData2)) == 0);
     VerifyOrQuit(memcmp(buffer + sizeof(kData1) + sizeof(kData2), kData1, sizeof(kData1)) == 0);
 
+    frameBuilder.Init(buffer, sizeof(buffer));
+    VerifyOrQuit(frameBuilder.GetBytes() == buffer);
+    VerifyOrQuit(frameBuilder.GetLength() == 0);
+    VerifyOrQuit(frameBuilder.GetMaxLength() == sizeof(buffer));
+
+    offset = 0;
+    SuccessOrQuit(frameBuilder.Insert(offset, kData1));
+    VerifyOrQuit(frameBuilder.GetLength() == sizeof(kData1));
+    VerifyOrQuit(frameBuilder.GetBytes() == buffer);
+    VerifyOrQuit(memcmp(buffer, kData1, sizeof(kData1)) == 0);
+
+    offset = 0;
+    SuccessOrQuit(frameBuilder.Insert(offset, kData2));
+    VerifyOrQuit(frameBuilder.GetLength() == sizeof(kData1) + sizeof(kData2));
+    VerifyOrQuit(frameBuilder.GetBytes() == buffer);
+    VerifyOrQuit(memcmp(buffer, kData2, sizeof(kData2)) == 0);
+    VerifyOrQuit(memcmp(buffer + sizeof(kData2), kData1, sizeof(kData1)) == 0);
+
+    offset = sizeof(kData2);
+    SuccessOrQuit(frameBuilder.InsertBytes(offset, kData3, sizeof(kData3)));
+    VerifyOrQuit(frameBuilder.GetLength() == sizeof(kData1) + sizeof(kData2) + sizeof(kData3));
+    VerifyOrQuit(frameBuilder.GetBytes() == buffer);
+    VerifyOrQuit(memcmp(buffer, kData2, sizeof(kData2)) == 0);
+    VerifyOrQuit(memcmp(buffer + sizeof(kData2), kData3, sizeof(kData3)) == 0);
+    VerifyOrQuit(memcmp(buffer + sizeof(kData2) + sizeof(kData3), kData1, sizeof(kData1)) == 0);
+
+    offset = frameBuilder.GetLength();
+    SuccessOrQuit(frameBuilder.Insert<uint8_t>(offset, 0x77));
+    VerifyOrQuit(frameBuilder.GetLength() == sizeof(kData1) + sizeof(kData2) + sizeof(kData3) + sizeof(uint8_t));
+    VerifyOrQuit(frameBuilder.GetBytes() == buffer);
+    VerifyOrQuit(memcmp(buffer, kData2, sizeof(kData2)) == 0);
+    VerifyOrQuit(memcmp(buffer + sizeof(kData2), kData3, sizeof(kData3)) == 0);
+    VerifyOrQuit(memcmp(buffer + sizeof(kData2) + sizeof(kData3), kData1, sizeof(kData1)) == 0);
+    VerifyOrQuit(buffer[sizeof(kData2) + sizeof(kData3) + sizeof(kData1)] == 0x77);
+
+    offset = frameBuilder.GetLength() - 1;
+    frameBuilder.RemoveBytes(offset, sizeof(uint8_t));
+    VerifyOrQuit(frameBuilder.GetLength() == sizeof(kData1) + sizeof(kData2) + sizeof(kData3));
+    VerifyOrQuit(frameBuilder.GetBytes() == buffer);
+    VerifyOrQuit(memcmp(buffer, kData2, sizeof(kData2)) == 0);
+    VerifyOrQuit(memcmp(buffer + sizeof(kData2), kData3, sizeof(kData3)) == 0);
+    VerifyOrQuit(memcmp(buffer + sizeof(kData2) + sizeof(kData3), kData1, sizeof(kData1)) == 0);
+
+    offset = sizeof(kData2);
+    frameBuilder.RemoveBytes(offset, sizeof(kData3));
+    VerifyOrQuit(frameBuilder.GetLength() == sizeof(kData1) + sizeof(kData2));
+    VerifyOrQuit(frameBuilder.GetBytes() == buffer);
+    VerifyOrQuit(memcmp(buffer, kData2, sizeof(kData2)) == 0);
+    VerifyOrQuit(memcmp(buffer + sizeof(kData2), kData1, sizeof(kData1)) == 0);
+
+    offset = 0;
+    frameBuilder.RemoveBytes(offset, sizeof(kData2));
+    VerifyOrQuit(frameBuilder.GetLength() == sizeof(kData1));
+    VerifyOrQuit(frameBuilder.GetBytes() == buffer);
+    VerifyOrQuit(memcmp(buffer, kData1, sizeof(kData1)) == 0);
+
+    offset = 0;
+    frameBuilder.RemoveBytes(offset, sizeof(kData1));
+    VerifyOrQuit(frameBuilder.GetLength() == 0);
+
     message->Free();
     testFreeInstance(instance);
 }
diff --git a/tests/unit/test_hdlc.cpp b/tests/unit/test_hdlc.cpp
index e820d5c..f23ae99 100644
--- a/tests/unit/test_hdlc.cpp
+++ b/tests/unit/test_hdlc.cpp
@@ -58,7 +58,7 @@
 static const uint8_t sHexText[]        = "0123456789abcdef";
 static const uint8_t sSkipText[]       = "Skip text";
 static const uint8_t sHdlcSpecials[]   = {kFlagSequence, kFlagXOn,        kFlagXOff,
-                                        kFlagSequence, kEscapeSequence, kFlagSpecial};
+                                          kFlagSequence, kEscapeSequence, kFlagSpecial};
 
 otError WriteToBuffer(const uint8_t *aText, Hdlc::FrameWritePointer &aWritePointer)
 {
@@ -126,8 +126,8 @@
 void TestHdlcMultiFrameBuffer(void)
 {
     Hdlc::MultiFrameBuffer<kBufferSize> frameBuffer;
-    uint8_t *                           frame    = nullptr;
-    uint8_t *                           newFrame = nullptr;
+    uint8_t                            *frame    = nullptr;
+    uint8_t                            *newFrame = nullptr;
     uint16_t                            length;
     uint16_t                            newLength;
 
@@ -454,7 +454,7 @@
     DecoderContext                      decoderContext;
     Hdlc::Encoder                       encoder(encoderBuffer);
     Hdlc::Decoder                       decoder(decoderBuffer, ProcessDecodedFrame, &decoderContext);
-    uint8_t *                           frame;
+    uint8_t                            *frame;
     uint16_t                            length;
     uint8_t                             badShortFrame[3] = {kFlagSequence, 0xaa, kFlagSequence};
 
@@ -591,10 +591,7 @@
     printf(" -- PASS\n");
 }
 
-uint32_t GetRandom(uint32_t max)
-{
-    return static_cast<uint32_t>(rand()) % max;
-}
+uint32_t GetRandom(uint32_t max) { return static_cast<uint32_t>(rand()) % max; }
 
 void TestFuzzEncoderDecoder(void)
 {
diff --git a/tests/unit/test_heap.cpp b/tests/unit/test_heap.cpp
index 813d200..b6cc884 100644
--- a/tests/unit/test_heap.cpp
+++ b/tests/unit/test_heap.cpp
@@ -82,7 +82,7 @@
 {
     struct Node
     {
-        Node * mNext;
+        Node  *mNext;
         size_t mSize;
     };
 
@@ -93,7 +93,7 @@
     srand(aSeed);
 
     const size_t totalSize = heap.GetFreeSize();
-    Node *       last      = &head;
+    Node        *last      = &head;
 
     do
     {
diff --git a/tests/unit/test_heap_array.cpp b/tests/unit/test_heap_array.cpp
index 2ed9535..bb95185 100644
--- a/tests/unit/test_heap_array.cpp
+++ b/tests/unit/test_heap_array.cpp
@@ -182,7 +182,7 @@
 {
     Heap::Array<uint16_t, 2> array;
     Heap::Array<uint16_t, 2> array2;
-    uint16_t *               entry;
+    uint16_t                *entry;
 
     printf("\n\n====================================================================================\n");
     printf("TestHeapArrayOfUint16\n\n");
@@ -348,7 +348,7 @@
     {
         Heap::Array<Entry, 2> array;
         Heap::Array<Entry, 2> array2;
-        Entry *               entry;
+        Entry                *entry;
 
         printf("------------------------------------------------------------------------------------\n");
         printf("After constructor\n");
diff --git a/tests/unit/test_heap_string.cpp b/tests/unit/test_heap_string.cpp
index b7973c2..cc72d5a 100644
--- a/tests/unit/test_heap_string.cpp
+++ b/tests/unit/test_heap_string.cpp
@@ -86,7 +86,7 @@
 {
     Heap::String str1;
     Heap::String str2;
-    const char * oldBuffer;
+    const char  *oldBuffer;
 
     printf("====================================================================================\n");
     printf("TestHeapString\n\n");
@@ -150,10 +150,7 @@
     printf("\n -- PASS\n");
 }
 
-void PrintData(const Heap::Data &aData)
-{
-    DumpBuffer("data", aData.GetBytes(), aData.GetLength());
-}
+void PrintData(const Heap::Data &aData) { DumpBuffer("data", aData.GetBytes(), aData.GetLength()); }
 
 static const uint8_t kTestValue = 0x77;
 
@@ -199,9 +196,9 @@
 
 void TestHeapData(void)
 {
-    Instance *     instance;
-    MessagePool *  messagePool;
-    Message *      message;
+    Instance      *instance;
+    MessagePool   *messagePool;
+    Message       *message;
     Heap::Data     data;
     uint16_t       offset;
     const uint8_t *oldBuffer;
diff --git a/tests/unit/test_hkdf_sha256.cpp b/tests/unit/test_hkdf_sha256.cpp
index ca12be6..a50ac84 100644
--- a/tests/unit/test_hkdf_sha256.cpp
+++ b/tests/unit/test_hkdf_sha256.cpp
@@ -59,7 +59,7 @@
     // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Test Case #1: RFC-5869 Appendix A.1
     const uint8_t kInKey1[]  = {0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
-                               0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b};
+                                0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b};
     const uint8_t kSalt1[]   = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c};
     const uint8_t kInfo1[]   = {0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9};
     const uint8_t kOutKey1[] = {0x3c, 0xb2, 0x5f, 0x25, 0xfa, 0xac, 0xd5, 0x7a, 0x90, 0x43, 0x4f, 0x64, 0xd0, 0x36,
diff --git a/tests/unit/test_hmac_sha256.cpp b/tests/unit/test_hmac_sha256.cpp
index 5c9a978..7b9e20a 100644
--- a/tests/unit/test_hmac_sha256.cpp
+++ b/tests/unit/test_hmac_sha256.cpp
@@ -72,7 +72,7 @@
 
     struct TestCase
     {
-        const char *       mData; // (null-terminated string).
+        const char        *mData; // (null-terminated string).
         otCryptoSha256Hash mHash;
     };
 
@@ -85,9 +85,9 @@
 
     printf("TestSha256\n");
 
-    Instance *   instance = testInitInstance();
+    Instance    *instance = testInitInstance();
     MessagePool *messagePool;
-    Message *    message;
+    Message     *message;
     uint16_t     offsets[GetArrayLength(kTestCases)];
     uint8_t      index;
 
@@ -142,7 +142,7 @@
     struct TestCase
     {
         otCryptoKey        mKey;
-        const void *       mData;
+        const void        *mData;
         uint16_t           mDataLength;
         otCryptoSha256Hash mHash;
     };
@@ -226,9 +226,9 @@
         {{&kKey5[0], sizeof(kKey5), 0}, kData5, sizeof(kData5) - 1, kHash5},
     };
 
-    Instance *   instance = testInitInstance();
+    Instance    *instance = testInitInstance();
     MessagePool *messagePool;
-    Message *    message;
+    Message     *message;
     uint16_t     offsets[GetArrayLength(kTestCases)];
     uint8_t      index;
 
diff --git a/tests/unit/test_ip_address.cpp b/tests/unit/test_ip_address.cpp
index b25cef9..06cef10 100644
--- a/tests/unit/test_ip_address.cpp
+++ b/tests/unit/test_ip_address.cpp
@@ -29,6 +29,7 @@
 #include <limits.h>
 
 #include "common/encoding.hpp"
+#include "common/string.hpp"
 #include "net/ip4_types.hpp"
 #include "net/ip6_address.hpp"
 
@@ -36,7 +37,7 @@
 
 template <typename AddressType> struct TestVector
 {
-    const char *  mString;
+    const char   *mString;
     const uint8_t mAddr[sizeof(AddressType)];
     ot::Error     mError;
 };
@@ -166,6 +167,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 +245,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.
@@ -215,6 +338,27 @@
     return matches;
 }
 
+bool CheckPrefixInIid(const ot::Ip6::InterfaceIdentifier &aIid, const uint8_t *aPrefix, uint8_t aPrefixLength)
+{
+    // Check the IID to contain the prefix bits (applicable when prefix length is longer than 64).
+
+    bool matches = true;
+
+    for (uint8_t bit = 64; bit < aPrefixLength; bit++)
+    {
+        uint8_t index = bit / CHAR_BIT;
+        uint8_t mask  = (0x80 >> (bit % CHAR_BIT));
+
+        if ((aIid.mFields.m8[index - 8] & mask) != (aPrefix[index] & mask))
+        {
+            matches = false;
+            break;
+        }
+    }
+
+    return matches;
+}
+
 bool CheckInterfaceId(const ot::Ip6::Address &aAddress1, const ot::Ip6::Address &aAddress2, uint8_t aPrefixLength)
 {
     // Check whether all the bits after aPrefixLength of the two given IPv6 Address match or not.
@@ -248,6 +392,7 @@
     ot::Ip6::Address address;
     ot::Ip6::Address allZeroAddress;
     ot::Ip6::Address allOneAddress;
+    ot::Ip6::Prefix  ip6Prefix;
 
     allZeroAddress.Clear();
     memset(&allOneAddress, 0xff, sizeof(allOneAddress));
@@ -259,18 +404,33 @@
 
         for (uint8_t prefixLength = 0; prefixLength <= sizeof(ot::Ip6::Address) * CHAR_BIT; prefixLength++)
         {
+            ip6Prefix.Clear();
+            ip6Prefix.Set(prefix, prefixLength);
+
             address = allZeroAddress;
-            address.SetPrefix(prefix, prefixLength);
+            address.SetPrefix(ip6Prefix);
             printf("   prefix-len:%-3d --> %s\n", prefixLength, address.ToString().AsCString());
             VerifyOrQuit(CheckPrefix(address, prefix, prefixLength), "Prefix does not match after SetPrefix()");
             VerifyOrQuit(CheckInterfaceId(address, allZeroAddress, prefixLength),
                          "SetPrefix changed bits beyond the prefix length");
 
             address = allOneAddress;
-            address.SetPrefix(prefix, prefixLength);
+            address.SetPrefix(ip6Prefix);
             VerifyOrQuit(CheckPrefix(address, prefix, prefixLength), "Prefix does not match after SetPrefix()");
             VerifyOrQuit(CheckInterfaceId(address, allOneAddress, prefixLength),
                          "SetPrefix changed bits beyond the prefix length");
+
+            address = allZeroAddress;
+            address.GetIid().ApplyPrefix(ip6Prefix);
+            VerifyOrQuit(CheckPrefixInIid(address.GetIid(), prefix, prefixLength), "IID is not correct");
+            VerifyOrQuit(CheckInterfaceId(address, allZeroAddress, prefixLength),
+                         "Iid:ApplyPrefrix() changed bits beyond the prefix length");
+
+            address = allOneAddress;
+            address.GetIid().ApplyPrefix(ip6Prefix);
+            VerifyOrQuit(CheckPrefixInIid(address.GetIid(), prefix, prefixLength), "IID is not correct");
+            VerifyOrQuit(CheckInterfaceId(address, allOneAddress, prefixLength),
+                         "Iid:ApplyPrefrix() changed bits beyond the prefix length");
         }
     }
 }
@@ -395,6 +555,30 @@
             VerifyOrQuit(!(testCase.mPrefixB < testCase.mPrefixA));
         }
     }
+
+    // `IsLinkLocal()` - should contain `fe80::/10`.
+    VerifyOrQuit(PrefixFrom("fe80::", 10).IsLinkLocal());
+    VerifyOrQuit(PrefixFrom("fe80::", 11).IsLinkLocal());
+    VerifyOrQuit(PrefixFrom("fea0::", 16).IsLinkLocal());
+    VerifyOrQuit(!PrefixFrom("fe80::", 9).IsLinkLocal());
+    VerifyOrQuit(!PrefixFrom("ff80::", 10).IsLinkLocal());
+    VerifyOrQuit(!PrefixFrom("fe00::", 10).IsLinkLocal());
+    VerifyOrQuit(!PrefixFrom("fec0::", 10).IsLinkLocal());
+
+    // `IsMulticast()` - should contain `ff00::/8`.
+    VerifyOrQuit(PrefixFrom("ff00::", 8).IsMulticast());
+    VerifyOrQuit(PrefixFrom("ff80::", 9).IsMulticast());
+    VerifyOrQuit(PrefixFrom("ffff::", 16).IsMulticast());
+    VerifyOrQuit(!PrefixFrom("ff00::", 7).IsMulticast());
+    VerifyOrQuit(!PrefixFrom("fe00::", 8).IsMulticast());
+
+    // `IsUniqueLocal()` - should contain `fc00::/7`.
+    VerifyOrQuit(PrefixFrom("fc00::", 7).IsUniqueLocal());
+    VerifyOrQuit(PrefixFrom("fd00::", 8).IsUniqueLocal());
+    VerifyOrQuit(PrefixFrom("fc10::", 16).IsUniqueLocal());
+    VerifyOrQuit(!PrefixFrom("fc00::", 6).IsUniqueLocal());
+    VerifyOrQuit(!PrefixFrom("f800::", 7).IsUniqueLocal());
+    VerifyOrQuit(!PrefixFrom("fe00::", 7).IsUniqueLocal());
 }
 
 void TestIp4Ip6Translation(void)
@@ -406,7 +590,7 @@
         const char *mIp6Address; // Expected IPv6 address (with embedded IPv4 "192.0.2.33").
     };
 
-    // The test cases are from RFC 6502 - section 2.4
+    // The test cases are from RFC 6052 - section 2.4
 
     const TestCase kTestCases[] = {
         {"2001:db8::", 32, "2001:db8:c000:221::"},
@@ -468,10 +652,10 @@
     using ot::Encoding::BigEndian::HostSwap32;
     struct TestCase
     {
-        const char *   mNetwork;
+        const char    *mNetwork;
         const uint8_t  mLength;
         const uint32_t mHost;
-        const char *   mOutcome;
+        const char    *mOutcome;
     };
 
     const TestCase kTestCases[] = {
@@ -519,9 +703,11 @@
     TestIp6AddressSetPrefix();
     TestIp4AddressFromString();
     TestIp6AddressFromString();
+    TestIp6PrefixFromString();
     TestIp6Prefix();
     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 f9162e3..0f2e9eb 100644
--- a/tests/unit/test_link_quality.cpp
+++ b/tests/unit/test_link_quality.cpp
@@ -31,6 +31,7 @@
 
 #include "common/array.hpp"
 #include "common/code_utils.hpp"
+#include "thread/link_metrics.hpp"
 #include "thread/link_quality.hpp"
 
 namespace ot {
@@ -69,7 +70,7 @@
 // Check and verify the raw average RSS value to match the value from GetAverage().
 void VerifyRawRssValue(int8_t aAverage, uint16_t aRawValue)
 {
-    if (aAverage != OT_RADIO_RSSI_INVALID)
+    if (aAverage != Radio::kInvalidRssi)
     {
         VerifyOrQuit(aAverage == -static_cast<int16_t>((aRawValue + (kRawAverageMultiple / 2)) >> kRawAverageBitShift),
                      "Raw value does not match the average.");
@@ -81,10 +82,7 @@
 }
 
 // This function prints the values in the passed in link info instance. It is invoked as the final step in test-case.
-void PrintOutcome(LinkQualityInfo &aLinkInfo)
-{
-    printf("%s -> PASS \n", aLinkInfo.ToInfoString().AsCString());
-}
+void PrintOutcome(LinkQualityInfo &aLinkInfo) { printf("%s -> PASS \n", aLinkInfo.ToInfoString().AsCString()); }
 
 void TestLinkQualityData(RssTestData aRssData)
 {
@@ -125,7 +123,7 @@
     int8_t   average  = aRssAverager.GetAverage();
     uint16_t rawValue = aRssAverager.GetRaw();
 
-    if (average != OT_RADIO_RSSI_INVALID)
+    if (average != Radio::kInvalidRssi)
     {
         VerifyOrQuit(average == -static_cast<int16_t>((rawValue + (kRawAverageMultiple / 2)) >> kRawAverageBitShift),
                      "Raw value does not match the average.");
@@ -137,10 +135,7 @@
 }
 
 // This function prints the values in the passed link info instance. It is invoked as the final step in test-case.
-void PrintOutcome(RssAverager &aRssAverager)
-{
-    printf("%s -> PASS\n", aRssAverager.ToString().AsCString());
-}
+void PrintOutcome(RssAverager &aRssAverager) { printf("%s -> PASS\n", aRssAverager.ToString().AsCString()); }
 
 int8_t GetRandomRss(void)
 {
@@ -165,7 +160,7 @@
     rssAverager.Clear();
 
     printf("\nAfter Clear: ");
-    VerifyOrQuit(rssAverager.GetAverage() == OT_RADIO_RSSI_INVALID, "Initial value from GetAverage() is incorrect.");
+    VerifyOrQuit(rssAverager.GetAverage() == Radio::kInvalidRssi, "Initial value from GetAverage() is incorrect.");
     VerifyRawRssValue(rssAverager);
     PrintOutcome(rssAverager);
 
@@ -183,7 +178,7 @@
 
     printf("Clear(): ");
     rssAverager.Clear();
-    VerifyOrQuit(rssAverager.GetAverage() == OT_RADIO_RSSI_INVALID, "GetAverage() after Clear() is incorrect.");
+    VerifyOrQuit(rssAverager.GetAverage() == Radio::kInvalidRssi, "GetAverage() after Clear() is incorrect.");
     VerifyRawRssValue(rssAverager);
     PrintOutcome(rssAverager);
 
@@ -338,30 +333,30 @@
 {
     const int8_t      rssList1[] = {-81, -80, -79, -78, -76, -80, -77, -75, -77, -76, -77, -74};
     const RssTestData rssData1   = {
-        rssList1,         // mRssList
-        sizeof(rssList1), // mRssListSize
-        3                 // mExpectedLinkQuality
+          rssList1,         // mRssList
+          sizeof(rssList1), // mRssListSize
+          3                 // mExpectedLinkQuality
     };
 
     const int8_t      rssList2[] = {-90, -80, -85};
     const RssTestData rssData2   = {
-        rssList2,         // mRssList
-        sizeof(rssList2), // mRssListSize
-        2                 // mExpectedLinkQuality
+          rssList2,         // mRssList
+          sizeof(rssList2), // mRssListSize
+          2                 // mExpectedLinkQuality
     };
 
     const int8_t      rssList3[] = {-95, -96, -98, -99, -100, -100, -98, -99, -100, -100, -100, -100, -100};
     const RssTestData rssData3   = {
-        rssList3,         // mRssList
-        sizeof(rssList3), // mRssListSize
-        0                 // mExpectedLinkQuality
+          rssList3,         // mRssList
+          sizeof(rssList3), // mRssListSize
+          0                 // mExpectedLinkQuality
     };
 
     const int8_t      rssList4[] = {-75, -100, -100, -100, -100, -100, -95, -92, -93, -94, -93, -93};
     const RssTestData rssData4   = {
-        rssList4,         // mRssList
-        sizeof(rssList4), // mRssListSize
-        1                 // mExpectedLinkQuality
+          rssList4,         // mRssList
+          sizeof(rssList4), // mRssListSize
+          1                 // mExpectedLinkQuality
     };
 
     TestLinkQualityData(rssData1);
@@ -481,6 +476,53 @@
     }
 }
 
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+
+class UnitTester
+{
+public:
+    static void TestLinkMetricsScaling(void)
+    {
+        printf("\nTestLinkMetricsScaling\n");
+
+        // Test Link Margin scaling from [0,130] -> [0, 255]
+
+        for (uint8_t linkMargin = 0; linkMargin <= 130; linkMargin++)
+        {
+            double  scaled     = 255.0 / 130.0 * linkMargin;
+            uint8_t scaledAsU8 = static_cast<uint8_t>(scaled + 0.5);
+
+            printf("\nLinkMargin : %-3u -> Scaled : %.1f (rounded:%u)", linkMargin, scaled, scaledAsU8);
+
+            VerifyOrQuit(LinkMetrics::ScaleLinkMarginToRawValue(linkMargin) == scaledAsU8);
+            VerifyOrQuit(LinkMetrics::ScaleRawValueToLinkMargin(scaledAsU8) == linkMargin);
+        }
+
+        VerifyOrQuit(LinkMetrics::ScaleLinkMarginToRawValue(131) == 255);
+        VerifyOrQuit(LinkMetrics::ScaleLinkMarginToRawValue(150) == 255);
+        VerifyOrQuit(LinkMetrics::ScaleLinkMarginToRawValue(255) == 255);
+
+        // Test RSSI scaling from [-130, 0] -> [0, 255]
+
+        for (int8_t rssi = -128; rssi <= 0; rssi++)
+        {
+            double  scaled     = 255.0 / 130.0 * (rssi + 130.0);
+            uint8_t scaledAsU8 = static_cast<uint8_t>(scaled + 0.5);
+
+            printf("\nRSSI : %-3d -> Scaled :%.1f (rounded:%u)", rssi, scaled, scaledAsU8);
+
+            VerifyOrQuit(LinkMetrics::ScaleRssiToRawValue(rssi) == scaledAsU8);
+            VerifyOrQuit(LinkMetrics::ScaleRawValueToRssi(scaledAsU8) == rssi);
+        }
+
+        VerifyOrQuit(LinkMetrics::ScaleRssiToRawValue(1) == 255);
+        VerifyOrQuit(LinkMetrics::ScaleRssiToRawValue(10) == 255);
+        VerifyOrQuit(LinkMetrics::ScaleRssiToRawValue(127) == 255);
+    }
+};
+
+#endif
+
 } // namespace ot
 
 int main(void)
@@ -488,6 +530,10 @@
     ot::TestRssAveraging();
     ot::TestLinkQualityCalculations();
     ot::TestSuccessRateTracker();
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+    ot::UnitTester::TestLinkMetricsScaling();
+#endif
+
     printf("\nAll tests passed\n");
     return 0;
 }
diff --git a/tests/unit/test_linked_list.cpp b/tests/unit/test_linked_list.cpp
index f84f63d..40a4f7f 100644
--- a/tests/unit/test_linked_list.cpp
+++ b/tests/unit/test_linked_list.cpp
@@ -88,8 +88,8 @@
 void VerifyLinkedListContent(const LinkedList<Entry> *aList, ...)
 {
     va_list      args;
-    Entry *      argEntry;
-    Entry *      argPrev = nullptr;
+    Entry       *argEntry;
+    Entry       *argPrev = nullptr;
     const Entry *prev;
     uint16_t     unusedId = 100;
 
@@ -134,7 +134,7 @@
 {
     Entry             a("a", 1, kAlphaType), b("b", 2, kAlphaType), c("c", 3, kBetaType);
     Entry             d("d", 4, kBetaType), e("e", 5, kAlphaType), f("f", 6, kBetaType);
-    Entry *           prev;
+    Entry            *prev;
     LinkedList<Entry> list;
     LinkedList<Entry> removedList;
 
diff --git a/tests/unit/test_lowpan.cpp b/tests/unit/test_lowpan.cpp
index beb6981..c28f8fd 100644
--- a/tests/unit/test_lowpan.cpp
+++ b/tests/unit/test_lowpan.cpp
@@ -35,8 +35,8 @@
 
 namespace ot {
 
-ot::Instance *  sInstance;
-Ip6::Ip6 *      sIp6;
+ot::Instance   *sInstance;
+Ip6::Ip6       *sIp6;
 Lowpan::Lowpan *sLowpan;
 
 void TestIphcVector::GetCompressedStream(uint8_t *aIphc, uint16_t &aIphcLength)
@@ -129,7 +129,8 @@
 
     SuccessOrQuit(message->AppendBytes(mockNetworkData, sizeof(mockNetworkData)));
 
-    IgnoreError(sInstance->Get<NetworkData::Leader>().SetNetworkData(0, 0, NetworkData::kStableSubset, *message, 0));
+    IgnoreError(
+        sInstance->Get<NetworkData::Leader>().SetNetworkData(0, 0, NetworkData::kStableSubset, *message, 2, 0x20));
 }
 
 /**
@@ -145,7 +146,7 @@
  */
 static void Test(TestIphcVector &aVector, bool aCompress, bool aDecompress)
 {
-    Message * message = nullptr;
+    Message  *message = nullptr;
     uint8_t   result[512];
     uint8_t   iphc[512];
     uint8_t   ip6[512];
@@ -172,7 +173,7 @@
     if (aCompress)
     {
         FrameBuilder frameBuilder;
-        Message *    compressedMsg;
+        Message     *compressedMsg;
         Ip6::Ecn     ecn;
 
         frameBuilder.Init(result, 127);
@@ -181,8 +182,7 @@
 
         aVector.GetUncompressedStream(*message);
 
-        VerifyOrQuit(sLowpan->Compress(*message, aVector.mMacSource, aVector.mMacDestination, frameBuilder) ==
-                     aVector.mError);
+        VerifyOrQuit(sLowpan->Compress(*message, aVector.mMacAddrs, frameBuilder) == aVector.mError);
 
         if (aVector.mError == kErrorNone)
         {
@@ -229,7 +229,7 @@
 
         frameData.Init(iphc, iphcLength);
 
-        error = sLowpan->Decompress(*message, aVector.mMacSource, aVector.mMacDestination, frameData, 0);
+        error = sLowpan->Decompress(*message, aVector.mMacAddrs, frameData, 0);
 
         message->ReadBytes(0, result, message->GetLength());
 
@@ -1870,6 +1870,7 @@
     uint8_t            frame[kMaxFrameSize];
     uint16_t           length;
     FrameData          frameData;
+    FrameBuilder       frameBuilder;
     Lowpan::MeshHeader meshHeader;
 
     meshHeader.Init(kSourceAddr, kDestAddr, 1);
@@ -1877,10 +1878,12 @@
     VerifyOrQuit(meshHeader.GetDestination() == kDestAddr, "failed after Init()");
     VerifyOrQuit(meshHeader.GetHopsLeft() == 1, "failed after Init()");
 
-    length = meshHeader.WriteTo(frame);
+    frameBuilder.Init(frame, sizeof(frame));
+    SuccessOrQuit(meshHeader.AppendTo(frameBuilder));
+    length = frameBuilder.GetLength();
     VerifyOrQuit(length == meshHeader.GetHeaderLength());
-    VerifyOrQuit(length == sizeof(kMeshHeader1), "MeshHeader::WriteTo() returned length is incorrect");
-    VerifyOrQuit(memcmp(frame, kMeshHeader1, length) == 0, "MeshHeader::WriteTo() failed");
+    VerifyOrQuit(length == sizeof(kMeshHeader1), "MeshHeader::AppendTo() returned length is incorrect");
+    VerifyOrQuit(memcmp(frame, kMeshHeader1, length) == 0, "MeshHeader::AppendTo() failed");
 
     memset(&meshHeader, 0, sizeof(meshHeader));
     frameData.Init(frame, length);
@@ -1903,10 +1906,12 @@
     VerifyOrQuit(meshHeader.GetDestination() == kDestAddr, "failed after Init()");
     VerifyOrQuit(meshHeader.GetHopsLeft() == 0x20, "failed after Init()");
 
-    length = meshHeader.WriteTo(frame);
-    VerifyOrQuit(length == sizeof(kMeshHeader2), "MeshHeader::WriteTo() returned length is incorrect");
+    frameBuilder.Init(frame, sizeof(frame));
+    SuccessOrQuit(meshHeader.AppendTo(frameBuilder));
+    length = frameBuilder.GetLength();
+    VerifyOrQuit(length == sizeof(kMeshHeader2), "MeshHeader::AppendTo() returned length is incorrect");
     VerifyOrQuit(length == meshHeader.GetHeaderLength());
-    VerifyOrQuit(memcmp(frame, kMeshHeader2, length) == 0, "MeshHeader::WriteTo() failed");
+    VerifyOrQuit(memcmp(frame, kMeshHeader2, length) == 0, "MeshHeader::AppendTo() failed");
 
     memset(&meshHeader, 0, sizeof(meshHeader));
     frameData.Init(frame, length);
@@ -1933,7 +1938,9 @@
     VerifyOrQuit(meshHeader.GetDestination() == kDestAddr, "failed after ParseFrom()");
     VerifyOrQuit(meshHeader.GetHopsLeft() == 1, "failed after ParseFrom()");
 
-    VerifyOrQuit(meshHeader.WriteTo(frame) == sizeof(kMeshHeader1));
+    frameBuilder.Init(frame, sizeof(frame));
+    SuccessOrQuit(meshHeader.AppendTo(frameBuilder));
+    VerifyOrQuit(frameBuilder.GetLength() == sizeof(kMeshHeader1));
 
     frameData.Init(kMeshHeader3, sizeof(kMeshHeader3) - 1);
     VerifyOrQuit(meshHeader.ParseFrom(frameData) == kErrorParse,
@@ -1942,13 +1949,10 @@
 
 void TestLowpanFragmentHeader(void)
 {
-    enum
-    {
-        kMaxFrameSize = 127,
-        kSize         = 0x7ef,
-        kTag          = 0x1234,
-        kOffset       = (100 * 8),
-    };
+    static constexpr uint16_t kMaxFrameSize = 127;
+    static constexpr uint16_t kSize         = 0x7ef;
+    static constexpr uint16_t kTag          = 0x1234;
+    static constexpr uint16_t kOffset       = (100 * 8);
 
     const uint8_t kFragHeader1[] = {0xc7, 0xef, 0x12, 0x34};       // size:0x7ef, tag:0x1234, offset:0 (first frag)
     const uint8_t kFragHeader2[] = {0xe7, 0xef, 0x12, 0x34, 0x64}; // size:0x7ef, tag:0x1234, offset:100 (next frag)
@@ -1958,21 +1962,22 @@
     const uint8_t kInvalidFragHeader2[] = {0xd0, 0xef, 0x12, 0x34, 0x64};
     const uint8_t kInvalidFragHeader3[] = {0x90, 0xef, 0x12, 0x34, 0x64};
 
-    uint8_t                frame[kMaxFrameSize];
-    uint16_t               length;
-    FrameData              frameData;
-    Lowpan::FragmentHeader fragHeader;
+    uint8_t                           frame[kMaxFrameSize];
+    uint16_t                          length;
+    FrameData                         frameData;
+    FrameBuilder                      frameBuilder;
+    Lowpan::FragmentHeader            fragHeader;
+    Lowpan::FragmentHeader::FirstFrag firstFragHeader;
+    Lowpan::FragmentHeader::NextFrag  nextFragHeader;
 
-    fragHeader.InitFirstFragment(kSize, kTag);
-    VerifyOrQuit(fragHeader.GetDatagramSize() == kSize, "failed after Init");
-    VerifyOrQuit(fragHeader.GetDatagramTag() == kTag, "failed after Init()");
-    VerifyOrQuit(fragHeader.GetDatagramOffset() == 0, "failed after Init()");
+    frameBuilder.Init(frame, sizeof(frame));
 
-    length = fragHeader.WriteTo(frame);
-    VerifyOrQuit(length == Lowpan::FragmentHeader::kFirstFragmentHeaderSize,
-                 "FragmentHeader::WriteTo() returned length is incorrect");
-    VerifyOrQuit(length == sizeof(kFragHeader1), "FragmentHeader::WriteTo() returned length is incorrect");
-    VerifyOrQuit(memcmp(frame, kFragHeader1, length) == 0, "FragmentHeader::WriteTo() failed");
+    firstFragHeader.Init(kSize, kTag);
+    SuccessOrQuit(frameBuilder.Append(firstFragHeader));
+
+    length = frameBuilder.GetLength();
+    VerifyOrQuit(length == sizeof(Lowpan::FragmentHeader::FirstFrag));
+    VerifyOrQuit(memcmp(frame, kFragHeader1, length) == 0);
 
     memset(&fragHeader, 0, sizeof(fragHeader));
 
@@ -1993,22 +1998,27 @@
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - -
 
-    fragHeader.Init(kSize, kTag, kOffset);
-    VerifyOrQuit(fragHeader.GetDatagramSize() == kSize, "failed after Init");
-    VerifyOrQuit(fragHeader.GetDatagramTag() == kTag, "failed after Init()");
-    VerifyOrQuit(fragHeader.GetDatagramOffset() == kOffset, "failed after Init()");
+    frameBuilder.Init(frame, sizeof(frame));
+    nextFragHeader.Init(kSize, kTag, kOffset);
+    SuccessOrQuit(frameBuilder.Append(nextFragHeader));
+    length = frameBuilder.GetLength();
+    VerifyOrQuit(length == sizeof(kFragHeader2));
+    VerifyOrQuit(memcmp(frame, kFragHeader2, length) == 0);
 
     // Check the truncation of offset (to be multiple of 8).
-    fragHeader.Init(kSize, kTag, kOffset + 1);
-    VerifyOrQuit(fragHeader.GetDatagramOffset() == kOffset, "FragmentHeader::GetDatagramOffset() did not truncate");
-    fragHeader.Init(kSize, kTag, kOffset + 7);
-    VerifyOrQuit(fragHeader.GetDatagramOffset() == kOffset, "FragmentHeader::GetDatagramOffset() did not truncate");
+    frameBuilder.Init(frame, sizeof(frame));
+    nextFragHeader.Init(kSize, kTag, kOffset + 1);
+    SuccessOrQuit(frameBuilder.Append(nextFragHeader));
+    length = frameBuilder.GetLength();
+    VerifyOrQuit(length == sizeof(kFragHeader2));
+    VerifyOrQuit(memcmp(frame, kFragHeader2, length) == 0);
 
-    length = fragHeader.WriteTo(frame);
-    VerifyOrQuit(length == Lowpan::FragmentHeader::kSubsequentFragmentHeaderSize,
-                 "FragmentHeader::WriteTo() returned length is incorrect");
-    VerifyOrQuit(length == sizeof(kFragHeader2), "FragmentHeader::WriteTo() returned length is incorrect");
-    VerifyOrQuit(memcmp(frame, kFragHeader2, length) == 0, "FragmentHeader::WriteTo() failed");
+    frameBuilder.Init(frame, sizeof(frame));
+    nextFragHeader.Init(kSize, kTag, kOffset + 7);
+    SuccessOrQuit(frameBuilder.Append(nextFragHeader));
+    length = frameBuilder.GetLength();
+    VerifyOrQuit(length == sizeof(kFragHeader2));
+    VerifyOrQuit(memcmp(frame, kFragHeader2, length) == 0);
 
     memset(&fragHeader, 0, sizeof(fragHeader));
     frameData.Init(frame, length);
diff --git a/tests/unit/test_lowpan.hpp b/tests/unit/test_lowpan.hpp
index b06fb07..1625dbc 100644
--- a/tests/unit/test_lowpan.hpp
+++ b/tests/unit/test_lowpan.hpp
@@ -73,7 +73,7 @@
      * @param aAddress Pointer to the long MAC address.
      *
      */
-    void SetMacSource(const uint8_t *aAddress) { mMacSource.SetExtended(aAddress); }
+    void SetMacSource(const uint8_t *aAddress) { mMacAddrs.mSource.SetExtended(aAddress); }
 
     /**
      * This method sets short MAC source address.
@@ -81,7 +81,7 @@
      * @param aAddress Short MAC address.
      *
      */
-    void SetMacSource(uint16_t aAddress) { mMacSource.SetShort(aAddress); }
+    void SetMacSource(uint16_t aAddress) { mMacAddrs.mSource.SetShort(aAddress); }
 
     /**
      * This method sets long MAC destination address.
@@ -89,7 +89,7 @@
      * @param aAddress Pointer to the long MAC address.
      *
      */
-    void SetMacDestination(const uint8_t *aAddress) { mMacDestination.SetExtended(aAddress); }
+    void SetMacDestination(const uint8_t *aAddress) { mMacAddrs.mDestination.SetExtended(aAddress); }
 
     /**
      * This method sets short MAC destination address.
@@ -97,7 +97,7 @@
      * @param aAddress Short MAC address.
      *
      */
-    void SetMacDestination(uint16_t aAddress) { mMacDestination.SetShort(aAddress); }
+    void SetMacDestination(uint16_t aAddress) { mMacAddrs.mDestination.SetShort(aAddress); }
 
     /**
      * This method gets the IPv6 header
@@ -261,8 +261,7 @@
      * This fields represent uncompressed IPv6 packet.
      *
      */
-    Mac::Address     mMacSource;
-    Mac::Address     mMacDestination;
+    Mac::Addresses   mMacAddrs;
     Ip6::Header      mIpHeader;
     Payload          mExtHeader;
     Ip6::Header      mIpTunneledHeader;
diff --git a/tests/unit/test_mac_frame.cpp b/tests/unit/test_mac_frame.cpp
index 5aac167..7966b70 100644
--- a/tests/unit/test_mac_frame.cpp
+++ b/tests/unit/test_mac_frame.cpp
@@ -53,12 +53,36 @@
     return matches;
 }
 
+bool CompareAddresses(const Mac::Address &aFirst, const Mac::Address &aSecond)
+{
+    bool matches = false;
+
+    VerifyOrExit(aFirst.GetType() == aSecond.GetType());
+
+    switch (aFirst.GetType())
+    {
+    case Mac::Address::kTypeNone:
+        break;
+    case Mac::Address::kTypeShort:
+        VerifyOrExit(aFirst.GetShort() == aSecond.GetShort());
+        break;
+    case Mac::Address::kTypeExtended:
+        VerifyOrExit(aFirst.GetExtended() == aSecond.GetExtended());
+        break;
+    }
+
+    matches = true;
+
+exit:
+    return matches;
+}
+
 void TestMacAddress(void)
 {
     const uint8_t           kExtAddr[OT_EXT_ADDRESS_SIZE] = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0};
     const Mac::ShortAddress kShortAddr                    = 0x1234;
 
-    ot::Instance *  instance;
+    ot::Instance   *instance;
     Mac::Address    addr;
     Mac::ExtAddress extAddr;
     uint8_t         buffer[OT_EXT_ADDRESS_SIZE];
@@ -156,57 +180,186 @@
 
 void TestMacHeader(void)
 {
-    static const struct
+    enum AddrType : uint8_t
     {
-        uint16_t fcf;
-        uint8_t  secCtl;
-        uint8_t  headerLength;
-        uint8_t  footerLength;
-    } tests[] = {
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrNone | Mac::Frame::kFcfSrcAddrNone, 0, 3, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrNone | Mac::Frame::kFcfSrcAddrShort, 0, 7, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrNone | Mac::Frame::kFcfSrcAddrExt, 0, 13, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrShort | Mac::Frame::kFcfSrcAddrNone, 0, 7, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrExt | Mac::Frame::kFcfSrcAddrNone, 0, 13, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrShort | Mac::Frame::kFcfSrcAddrShort, 0, 11, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrShort | Mac::Frame::kFcfSrcAddrExt, 0, 17, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrExt | Mac::Frame::kFcfSrcAddrShort, 0, 17, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrExt | Mac::Frame::kFcfSrcAddrExt, 0, 23, 2},
-
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrShort | Mac::Frame::kFcfSrcAddrShort |
-             Mac::Frame::kFcfPanidCompression,
-         0, 9, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrShort | Mac::Frame::kFcfSrcAddrExt |
-             Mac::Frame::kFcfPanidCompression,
-         0, 15, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrExt | Mac::Frame::kFcfSrcAddrShort |
-             Mac::Frame::kFcfPanidCompression,
-         0, 15, 2},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrExt | Mac::Frame::kFcfSrcAddrExt |
-             Mac::Frame::kFcfPanidCompression,
-         0, 21, 2},
-
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrShort | Mac::Frame::kFcfSrcAddrShort |
-             Mac::Frame::kFcfPanidCompression | Mac::Frame::kFcfSecurityEnabled,
-         Mac::Frame::kSecMic32 | Mac::Frame::kKeyIdMode1, 15, 6},
-        {Mac::Frame::kFcfFrameVersion2006 | Mac::Frame::kFcfDstAddrShort | Mac::Frame::kFcfSrcAddrShort |
-             Mac::Frame::kFcfPanidCompression | Mac::Frame::kFcfSecurityEnabled,
-         Mac::Frame::kSecMic32 | Mac::Frame::kKeyIdMode2, 19, 6},
+        kNoneAddr,
+        kShrtAddr,
+        kExtdAddr,
     };
 
-    for (const auto &test : tests)
+    enum PanIdMode
     {
-        uint8_t      psdu[OT_RADIO_FRAME_MAX_SIZE];
-        Mac::TxFrame frame;
+        kSamePanIds,
+        kDiffPanIds,
+    };
+
+    struct TestCase
+    {
+        Mac::Frame::Version       mVersion;
+        AddrType                  mSrcAddrType;
+        AddrType                  mDstAddrType;
+        PanIdMode                 mPanIdMode;
+        Mac::Frame::SecurityLevel mSecurity;
+        Mac::Frame::KeyIdMode     mKeyIdMode;
+        uint8_t                   mHeaderLength;
+        uint8_t                   mFooterLength;
+    };
+
+    static constexpr Mac::Frame::Version kVer2006 = Mac::Frame::kVersion2006;
+    static constexpr Mac::Frame::Version kVer2015 = Mac::Frame::kVersion2015;
+
+    static constexpr Mac::Frame::SecurityLevel kNoSec = Mac::Frame::kSecurityNone;
+    static constexpr Mac::Frame::SecurityLevel kMic32 = Mac::Frame::kSecurityMic32;
+
+    static constexpr Mac::Frame::KeyIdMode kModeId1 = Mac::Frame::kKeyIdMode1;
+    static constexpr Mac::Frame::KeyIdMode kModeId2 = Mac::Frame::kKeyIdMode2;
+
+    static constexpr TestCase kTestCases[] = {
+        {kVer2006, kNoneAddr, kNoneAddr, kSamePanIds, kNoSec, kModeId1, 3, 2},
+        {kVer2006, kShrtAddr, kNoneAddr, kSamePanIds, kNoSec, kModeId1, 7, 2},
+        {kVer2006, kExtdAddr, kNoneAddr, kSamePanIds, kNoSec, kModeId1, 13, 2},
+        {kVer2006, kNoneAddr, kShrtAddr, kSamePanIds, kNoSec, kModeId1, 7, 2},
+        {kVer2006, kNoneAddr, kExtdAddr, kSamePanIds, kNoSec, kModeId1, 13, 2},
+        {kVer2006, kShrtAddr, kShrtAddr, kDiffPanIds, kNoSec, kModeId1, 11, 2},
+        {kVer2006, kShrtAddr, kExtdAddr, kDiffPanIds, kNoSec, kModeId1, 17, 2},
+        {kVer2006, kExtdAddr, kShrtAddr, kDiffPanIds, kNoSec, kModeId1, 17, 2},
+        {kVer2006, kExtdAddr, kExtdAddr, kDiffPanIds, kNoSec, kModeId1, 23, 2},
+        {kVer2006, kShrtAddr, kShrtAddr, kSamePanIds, kNoSec, kModeId1, 9, 2},
+        {kVer2006, kShrtAddr, kExtdAddr, kSamePanIds, kNoSec, kModeId1, 15, 2},
+        {kVer2006, kExtdAddr, kShrtAddr, kSamePanIds, kNoSec, kModeId1, 15, 2},
+        {kVer2006, kExtdAddr, kExtdAddr, kSamePanIds, kNoSec, kModeId1, 21, 2},
+        {kVer2006, kShrtAddr, kShrtAddr, kSamePanIds, kMic32, kModeId1, 15, 6},
+        {kVer2006, kShrtAddr, kShrtAddr, kSamePanIds, kMic32, kModeId2, 19, 6},
+
+        {kVer2015, kNoneAddr, kNoneAddr, kSamePanIds, kNoSec, kModeId1, 3, 2},
+        {kVer2015, kShrtAddr, kNoneAddr, kSamePanIds, kNoSec, kModeId1, 7, 2},
+        {kVer2015, kExtdAddr, kNoneAddr, kSamePanIds, kNoSec, kModeId1, 13, 2},
+        {kVer2015, kNoneAddr, kShrtAddr, kSamePanIds, kNoSec, kModeId1, 7, 2},
+        {kVer2015, kNoneAddr, kExtdAddr, kSamePanIds, kNoSec, kModeId1, 13, 2},
+        {kVer2015, kShrtAddr, kShrtAddr, kDiffPanIds, kNoSec, kModeId1, 11, 2},
+        {kVer2015, kShrtAddr, kExtdAddr, kDiffPanIds, kNoSec, kModeId1, 17, 2},
+        {kVer2015, kExtdAddr, kShrtAddr, kDiffPanIds, kNoSec, kModeId1, 17, 2},
+        {kVer2015, kShrtAddr, kShrtAddr, kSamePanIds, kNoSec, kModeId1, 9, 2},
+        {kVer2015, kShrtAddr, kExtdAddr, kSamePanIds, kNoSec, kModeId1, 15, 2},
+        {kVer2015, kExtdAddr, kShrtAddr, kSamePanIds, kNoSec, kModeId1, 15, 2},
+        {kVer2015, kExtdAddr, kExtdAddr, kSamePanIds, kNoSec, kModeId1, 21, 2},
+        {kVer2015, kShrtAddr, kShrtAddr, kSamePanIds, kMic32, kModeId1, 15, 6},
+        {kVer2015, kShrtAddr, kShrtAddr, kSamePanIds, kMic32, kModeId2, 19, 6},
+    };
+
+    const uint16_t kPanId1     = 0xbaba;
+    const uint16_t kPanId2     = 0xdede;
+    const uint16_t kShortAddr1 = 0x1234;
+    const uint16_t kShortAddr2 = 0x5678;
+    const uint8_t  kExtAddr1[] = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0};
+    const uint8_t  kExtAddr2[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
+
+    Mac::ExtAddress extAddr1;
+    Mac::ExtAddress extAddr2;
+
+    extAddr1.Set(kExtAddr1);
+    extAddr2.Set(kExtAddr2);
+
+    printf("TestMacHeader\n");
+
+    for (const TestCase &testCase : kTestCases)
+    {
+        uint8_t        psdu[OT_RADIO_FRAME_MAX_SIZE];
+        Mac::TxFrame   frame;
+        Mac::Addresses addresses;
+        Mac::Address   address;
+        Mac::PanIds    panIds;
+        Mac::PanId     panId;
 
         frame.mPsdu      = psdu;
         frame.mLength    = 0;
         frame.mRadioType = 0;
 
-        frame.InitMacHeader(test.fcf, test.secCtl);
-        VerifyOrQuit(frame.GetHeaderLength() == test.headerLength);
-        VerifyOrQuit(frame.GetFooterLength() == test.footerLength);
-        VerifyOrQuit(frame.GetLength() == test.headerLength + test.footerLength);
+        switch (testCase.mSrcAddrType)
+        {
+        case kNoneAddr:
+            addresses.mSource.SetNone();
+            break;
+        case kShrtAddr:
+            addresses.mSource.SetShort(kShortAddr1);
+            break;
+        case kExtdAddr:
+            addresses.mSource.SetExtended(extAddr1);
+            break;
+        }
+
+        switch (testCase.mDstAddrType)
+        {
+        case kNoneAddr:
+            addresses.mDestination.SetNone();
+            break;
+        case kShrtAddr:
+            addresses.mDestination.SetShort(kShortAddr2);
+            break;
+        case kExtdAddr:
+            addresses.mDestination.SetExtended(extAddr2);
+            break;
+        }
+
+        switch (testCase.mPanIdMode)
+        {
+        case kSamePanIds:
+            panIds.mSource = panIds.mDestination = kPanId1;
+            break;
+        case kDiffPanIds:
+            panIds.mSource      = kPanId1;
+            panIds.mDestination = kPanId2;
+            break;
+        }
+
+        frame.InitMacHeader(Mac::Frame::kTypeData, testCase.mVersion, addresses, panIds, testCase.mSecurity,
+                            testCase.mKeyIdMode);
+
+        VerifyOrQuit(frame.GetHeaderLength() == testCase.mHeaderLength);
+        VerifyOrQuit(frame.GetFooterLength() == testCase.mFooterLength);
+        VerifyOrQuit(frame.GetLength() == testCase.mHeaderLength + testCase.mFooterLength);
+
+        VerifyOrQuit(frame.GetType() == Mac::Frame::kTypeData);
+        VerifyOrQuit(!frame.IsAck());
+        VerifyOrQuit(frame.GetVersion() == testCase.mVersion);
+        VerifyOrQuit(frame.GetSecurityEnabled() == (testCase.mSecurity != kNoSec));
+        VerifyOrQuit(!frame.GetFramePending());
+        VerifyOrQuit(!frame.IsIePresent());
+        VerifyOrQuit(frame.GetAckRequest() == (testCase.mDstAddrType != kNoneAddr));
+
+        VerifyOrQuit(frame.IsSrcAddrPresent() == (testCase.mSrcAddrType != kNoneAddr));
+        SuccessOrQuit(frame.GetSrcAddr(address));
+        VerifyOrQuit(CompareAddresses(address, addresses.mSource));
+        VerifyOrQuit(frame.IsDstAddrPresent() == (testCase.mDstAddrType != kNoneAddr));
+        SuccessOrQuit(frame.GetDstAddr(address));
+        VerifyOrQuit(CompareAddresses(address, addresses.mDestination));
+
+        if (testCase.mDstAddrType != kNoneAddr)
+        {
+            VerifyOrQuit(frame.IsDstPanIdPresent());
+            SuccessOrQuit(frame.GetDstPanId(panId));
+            VerifyOrQuit(panId == panIds.mDestination);
+        }
+
+        if (frame.IsSrcPanIdPresent())
+        {
+            SuccessOrQuit(frame.GetSrcPanId(panId));
+            VerifyOrQuit(panId == panIds.mSource);
+        }
+
+        if (frame.GetSecurityEnabled())
+        {
+            uint8_t security;
+            uint8_t keyIdMode;
+
+            SuccessOrQuit(frame.GetSecurityLevel(security));
+            VerifyOrQuit(security == testCase.mSecurity);
+
+            SuccessOrQuit(frame.GetKeyIdMode(keyIdMode));
+            VerifyOrQuit(keyIdMode == testCase.mKeyIdMode);
+        }
+
+        printf(" %d, %d\n", testCase.mHeaderLength, testCase.mFooterLength);
     }
 }
 
@@ -358,7 +511,7 @@
 
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
     uint8_t data_psdu1[]    = {0x29, 0xee, 0x53, 0xce, 0xfa, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x6e, 0x16, 0x05,
-                            0x00, 0x00, 0x00, 0x00, 0x0a, 0x6e, 0x16, 0x0d, 0x01, 0x00, 0x00, 0x00, 0x01};
+                               0x00, 0x00, 0x00, 0x00, 0x0a, 0x6e, 0x16, 0x0d, 0x01, 0x00, 0x00, 0x00, 0x01};
     uint8_t mac_cmd_psdu2[] = {0x6b, 0xaa, 0x8d, 0xce, 0xfa, 0x00, 0x68, 0x01, 0x68, 0x0d,
                                0x08, 0x00, 0x00, 0x00, 0x01, 0x04, 0x0d, 0xed, 0x0b, 0x35,
                                0x0c, 0x80, 0x3f, 0x04, 0x4b, 0x88, 0x89, 0xd6, 0x59, 0xe1};
@@ -381,14 +534,14 @@
     //   FCS: 0x9bd2 (Correct)
     frame.mPsdu   = ack_psdu1;
     frame.mLength = sizeof(ack_psdu1);
-    VerifyOrQuit(frame.GetType() == Mac::Frame::kFcfFrameAck);
+    VerifyOrQuit(frame.GetType() == Mac::Frame::kTypeAck);
     VerifyOrQuit(!frame.GetSecurityEnabled());
     VerifyOrQuit(!frame.GetFramePending());
     VerifyOrQuit(!frame.GetAckRequest());
     VerifyOrQuit(!frame.IsIePresent());
     VerifyOrQuit(!frame.IsDstPanIdPresent());
     VerifyOrQuit(!frame.IsDstAddrPresent());
-    VerifyOrQuit(frame.GetVersion() == Mac::Frame::kFcfFrameVersion2006);
+    VerifyOrQuit(frame.GetVersion() == Mac::Frame::kVersion2006);
     VerifyOrQuit(!frame.IsSrcAddrPresent());
     VerifyOrQuit(frame.GetSequence() == 94);
 
@@ -420,8 +573,8 @@
     frame.mPsdu   = mac_cmd_psdu1;
     frame.mLength = sizeof(mac_cmd_psdu1);
     VerifyOrQuit(frame.GetSequence() == 133);
-    VerifyOrQuit(frame.GetVersion() == Mac::Frame::kFcfFrameVersion2006);
-    VerifyOrQuit(frame.GetType() == Mac::Frame::kFcfFrameMacCmd);
+    VerifyOrQuit(frame.GetVersion() == Mac::Frame::kVersion2006);
+    VerifyOrQuit(frame.GetType() == Mac::Frame::kTypeMacCmd);
     SuccessOrQuit(frame.GetCommandId(commandId));
     VerifyOrQuit(commandId == Mac::Frame::kMacCmdDataRequest);
     SuccessOrQuit(frame.SetCommandId(Mac::Frame::kMacCmdBeaconRequest));
@@ -439,7 +592,7 @@
     frame.mLength = sizeof(mac_cmd_psdu2);
     VerifyOrQuit(frame.GetSequence() == 141);
     VerifyOrQuit(frame.IsVersion2015());
-    VerifyOrQuit(frame.GetType() == Mac::Frame::kFcfFrameMacCmd);
+    VerifyOrQuit(frame.GetType() == Mac::Frame::kTypeMacCmd);
     SuccessOrQuit(frame.GetCommandId(commandId));
     VerifyOrQuit(commandId == Mac::Frame::kMacCmdDataRequest);
     printf("commandId:%d\n", commandId);
@@ -452,6 +605,8 @@
 
 void TestMacFrameAckGeneration(void)
 {
+    constexpr uint8_t kImmAckLength = 5;
+
     Mac::RxFrame receivedFrame;
     Mac::TxFrame ackFrame;
     uint8_t      ackFrameBuffer[100];
@@ -477,18 +632,18 @@
     //  Destination: 16:6e:0a:00:00:00:00:01 (16:6e:0a:00:00:00:00:01)
     //  Extended Source: 16:6e:0a:00:00:00:00:02 (16:6e:0a:00:00:00:00:02)
     uint8_t data_psdu1[]  = {0x61, 0xdc, 0xbd, 0xce, 0xfa, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x6e, 0x16, 0x02,
-                            0x00, 0x00, 0x00, 0x00, 0x0a, 0x6e, 0x16, 0x7f, 0x33, 0xf0, 0x4d, 0x4c, 0x4d, 0x4c,
-                            0x8b, 0xf0, 0x00, 0x15, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc2,
-                            0x57, 0x9c, 0x31, 0xb3, 0x2a, 0xa1, 0x86, 0xba, 0x9a, 0xed, 0x5a, 0xb9, 0xa3, 0x59,
-                            0x88, 0xeb, 0xbb, 0x0d, 0xc3, 0xed, 0xeb, 0x8a, 0x53, 0xa6, 0xed, 0xf7, 0xdd, 0x45,
-                            0x6e, 0xf7, 0x9a, 0x17, 0xb4, 0xab, 0xc6, 0x75, 0x71, 0x46, 0x37, 0x93, 0x4a, 0x32,
-                            0xb1, 0x21, 0x9f, 0x9d, 0xb3, 0x65, 0x27, 0xd5, 0xfc, 0x50, 0x16, 0x90, 0xd2, 0xd4};
+                             0x00, 0x00, 0x00, 0x00, 0x0a, 0x6e, 0x16, 0x7f, 0x33, 0xf0, 0x4d, 0x4c, 0x4d, 0x4c,
+                             0x8b, 0xf0, 0x00, 0x15, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc2,
+                             0x57, 0x9c, 0x31, 0xb3, 0x2a, 0xa1, 0x86, 0xba, 0x9a, 0xed, 0x5a, 0xb9, 0xa3, 0x59,
+                             0x88, 0xeb, 0xbb, 0x0d, 0xc3, 0xed, 0xeb, 0x8a, 0x53, 0xa6, 0xed, 0xf7, 0xdd, 0x45,
+                             0x6e, 0xf7, 0x9a, 0x17, 0xb4, 0xab, 0xc6, 0x75, 0x71, 0x46, 0x37, 0x93, 0x4a, 0x32,
+                             0xb1, 0x21, 0x9f, 0x9d, 0xb3, 0x65, 0x27, 0xd5, 0xfc, 0x50, 0x16, 0x90, 0xd2, 0xd4};
     receivedFrame.mPsdu   = data_psdu1;
     receivedFrame.mLength = sizeof(data_psdu1);
 
     ackFrame.GenerateImmAck(receivedFrame, false);
-    VerifyOrQuit(ackFrame.mLength == Mac::Frame::kImmAckLength);
-    VerifyOrQuit(ackFrame.GetType() == Mac::Frame::kFcfFrameAck);
+    VerifyOrQuit(ackFrame.mLength == kImmAckLength);
+    VerifyOrQuit(ackFrame.GetType() == Mac::Frame::kTypeAck);
     VerifyOrQuit(!ackFrame.GetSecurityEnabled());
     VerifyOrQuit(!ackFrame.GetFramePending());
 
@@ -497,7 +652,7 @@
     VerifyOrQuit(!ackFrame.IsDstPanIdPresent());
     VerifyOrQuit(!ackFrame.IsDstAddrPresent());
     VerifyOrQuit(!ackFrame.IsSrcAddrPresent());
-    VerifyOrQuit(ackFrame.GetVersion() == Mac::Frame::kFcfFrameVersion2006);
+    VerifyOrQuit(ackFrame.GetVersion() == Mac::Frame::kVersion2006);
     VerifyOrQuit(ackFrame.GetSequence() == 189);
 
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
@@ -537,8 +692,8 @@
     //   [Key Number: 0]
     //   FCS: 0x8c40 (Correct)
     uint8_t data_psdu2[]  = {0x69, 0xa8, 0x8e, 0xce, 0xfa, 0x02, 0x24, 0x00, 0x24, 0x0d, 0x02,
-                            0x00, 0x00, 0x00, 0x01, 0x6b, 0x64, 0x60, 0x08, 0x55, 0xb8, 0x10,
-                            0x18, 0xc7, 0x40, 0x2e, 0xfb, 0xf3, 0xda, 0xf9, 0x4e, 0x58, 0x70};
+                             0x00, 0x00, 0x00, 0x01, 0x6b, 0x64, 0x60, 0x08, 0x55, 0xb8, 0x10,
+                             0x18, 0xc7, 0x40, 0x2e, 0xfb, 0xf3, 0xda, 0xf9, 0x4e, 0x58, 0x70};
     receivedFrame.mPsdu   = data_psdu2;
     receivedFrame.mLength = sizeof(data_psdu2);
 
@@ -548,13 +703,13 @@
     IgnoreError(ackFrame.GenerateEnhAck(receivedFrame, false, ie_data, sizeof(ie_data)));
     csl = reinterpret_cast<Mac::CslIe *>(ackFrame.GetHeaderIe(Mac::CslIe::kHeaderIeId) + sizeof(Mac::HeaderIe));
     VerifyOrQuit(ackFrame.mLength == 23);
-    VerifyOrQuit(ackFrame.GetType() == Mac::Frame::kFcfFrameAck);
+    VerifyOrQuit(ackFrame.GetType() == Mac::Frame::kTypeAck);
     VerifyOrQuit(ackFrame.GetSecurityEnabled());
     VerifyOrQuit(ackFrame.IsIePresent());
     VerifyOrQuit(!ackFrame.IsDstPanIdPresent());
     VerifyOrQuit(ackFrame.IsDstAddrPresent());
     VerifyOrQuit(!ackFrame.IsSrcAddrPresent());
-    VerifyOrQuit(ackFrame.GetVersion() == Mac::Frame::kFcfFrameVersion2015);
+    VerifyOrQuit(ackFrame.GetVersion() == Mac::Frame::kVersion2015);
     VerifyOrQuit(ackFrame.GetSequence() == 142);
     VerifyOrQuit(csl->GetPeriod() == 3125 && csl->GetPhase() == 3105);
 
diff --git a/tests/unit/test_macros.cpp b/tests/unit/test_macros.cpp
index 0db16ca..a8ebdc2 100644
--- a/tests/unit/test_macros.cpp
+++ b/tests/unit/test_macros.cpp
@@ -30,55 +30,25 @@
 
 #include "common/arg_macros.hpp"
 
-static constexpr uint8_t NumberOfArgs(void)
-{
-    return 0;
-}
+static constexpr uint8_t NumberOfArgs(void) { return 0; }
 
-static constexpr uint8_t NumberOfArgs(uint8_t)
-{
-    return 1;
-}
+static constexpr uint8_t NumberOfArgs(uint8_t) { return 1; }
 
-static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t)
-{
-    return 2;
-}
+static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t) { return 2; }
 
-static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t, uint8_t)
-{
-    return 3;
-}
+static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t, uint8_t) { return 3; }
 
-static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t, uint8_t, uint8_t)
-{
-    return 4;
-}
+static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t, uint8_t, uint8_t) { return 4; }
 
-static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t)
-{
-    return 5;
-}
+static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t) { return 5; }
 
-static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t)
-{
-    return 6;
-}
+static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t) { return 6; }
 
-static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t)
-{
-    return 7;
-}
+static constexpr uint8_t NumberOfArgs(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t) { return 7; }
 
-int Sum(int aFirst)
-{
-    return aFirst;
-}
+int Sum(int aFirst) { return aFirst; }
 
-template <typename... Args> int Sum(int aFirst, Args... aArgs)
-{
-    return aFirst + Sum(aArgs...);
-}
+template <typename... Args> int Sum(int aFirst, Args... aArgs) { return aFirst + Sum(aArgs...); }
 
 void TestMacros(void)
 {
diff --git a/tests/unit/test_message.cpp b/tests/unit/test_message.cpp
index fe2ce3f..d6aa9d3 100644
--- a/tests/unit/test_message.cpp
+++ b/tests/unit/test_message.cpp
@@ -46,10 +46,10 @@
         kLengthStep = 21,
     };
 
-    Instance *   instance;
+    Instance    *instance;
     MessagePool *messagePool;
-    Message *    message;
-    Message *    message2;
+    Message     *message;
+    Message     *message2;
     uint8_t      writeBuffer[kMaxSize];
     uint8_t      readBuffer[kMaxSize];
     uint8_t      zeroBuffer[kMaxSize];
@@ -134,63 +134,90 @@
 
     VerifyOrQuit(message->GetLength() == kMaxSize);
 
-    // Test `Message::CopyTo()` behavior.
+    // Test `WriteBytesFromMessage()` behavior copying between different
+    // messages.
 
     VerifyOrQuit((message2 = messagePool->Allocate(Message::kTypeIp6)) != nullptr);
     SuccessOrQuit(message2->SetLength(kMaxSize));
 
-    for (uint16_t srcOffset = 0; srcOffset < kMaxSize; srcOffset += kOffsetStep)
+    for (uint16_t readOffset = 0; readOffset < kMaxSize; readOffset += kOffsetStep)
     {
-        for (uint16_t dstOffset = 0; dstOffset < kMaxSize; dstOffset += kOffsetStep)
+        for (uint16_t writeOffset = 0; writeOffset < kMaxSize; writeOffset += kOffsetStep)
         {
-            for (uint16_t length = 0; length <= kMaxSize - dstOffset; length += kLengthStep)
+            for (uint16_t length = 0; length <= kMaxSize - Max(writeOffset, readOffset); length += kLengthStep)
             {
-                uint16_t bytesCopied;
-
                 message2->WriteBytes(0, zeroBuffer, kMaxSize);
 
-                bytesCopied = message->CopyTo(srcOffset, dstOffset, length, *message2);
-
-                if (srcOffset + length <= kMaxSize)
-                {
-                    VerifyOrQuit(bytesCopied == length, "CopyTo() failed");
-                }
-                else
-                {
-                    VerifyOrQuit(bytesCopied == kMaxSize - srcOffset, "CopyTo() failed");
-                }
+                message2->WriteBytesFromMessage(writeOffset, *message, readOffset, length);
 
                 SuccessOrQuit(message2->Read(0, readBuffer, kMaxSize));
 
-                VerifyOrQuit(memcmp(&readBuffer[0], zeroBuffer, dstOffset) == 0, "read before length");
-                VerifyOrQuit(memcmp(&readBuffer[dstOffset], &writeBuffer[srcOffset], bytesCopied) == 0);
-                VerifyOrQuit(
-                    memcmp(&readBuffer[dstOffset + bytesCopied], zeroBuffer, kMaxSize - bytesCopied - dstOffset) == 0,
-                    "read after length");
+                VerifyOrQuit(memcmp(&readBuffer[0], zeroBuffer, writeOffset) == 0);
+                VerifyOrQuit(memcmp(&readBuffer[writeOffset], &writeBuffer[readOffset], length) == 0);
+                VerifyOrQuit(memcmp(&readBuffer[writeOffset + length], zeroBuffer, kMaxSize - length - writeOffset) ==
+                             0);
 
-                VerifyOrQuit(message->CompareBytes(srcOffset, *message2, dstOffset, bytesCopied));
-                VerifyOrQuit(message2->CompareBytes(dstOffset, *message, srcOffset, bytesCopied));
+                VerifyOrQuit(message->CompareBytes(readOffset, *message2, writeOffset, length));
+                VerifyOrQuit(message2->CompareBytes(writeOffset, *message, readOffset, length));
             }
         }
     }
 
-    // Verify `CopyTo()` with same source and destination message and a backward copy.
+    // Verify `WriteBytesFromMessage()` behavior copying backwards within
+    // same message.
 
-    for (uint16_t srcOffset = 0; srcOffset < kMaxSize; srcOffset++)
+    for (uint16_t readOffset = 0; readOffset < kMaxSize; readOffset++)
     {
-        uint16_t bytesCopied;
+        uint16_t length = kMaxSize - readOffset;
 
         message->WriteBytes(0, writeBuffer, kMaxSize);
 
-        bytesCopied = message->CopyTo(srcOffset, 0, kMaxSize, *message);
-        VerifyOrQuit(bytesCopied == kMaxSize - srcOffset, "CopyTo() failed");
+        message->WriteBytesFromMessage(0, *message, readOffset, length);
 
         SuccessOrQuit(message->Read(0, readBuffer, kMaxSize));
 
-        VerifyOrQuit(memcmp(&readBuffer[0], &writeBuffer[srcOffset], bytesCopied) == 0,
-                     "CopyTo() changed before srcOffset");
-        VerifyOrQuit(memcmp(&readBuffer[bytesCopied], &writeBuffer[bytesCopied], kMaxSize - bytesCopied) == 0,
-                     "CopyTo() write error");
+        VerifyOrQuit(memcmp(&readBuffer[0], &writeBuffer[readOffset], length) == 0);
+        VerifyOrQuit(memcmp(&readBuffer[length], &writeBuffer[length], kMaxSize - length) == 0);
+    }
+
+    // Verify `WriteBytesFromMessage()` behavior copying forward within
+    // same message.
+
+    for (uint16_t writeOffset = 0; writeOffset < kMaxSize; writeOffset++)
+    {
+        uint16_t length = kMaxSize - writeOffset;
+
+        message->WriteBytes(0, writeBuffer, kMaxSize);
+
+        message->WriteBytesFromMessage(writeOffset, *message, 0, length);
+
+        SuccessOrQuit(message->Read(0, readBuffer, kMaxSize));
+
+        VerifyOrQuit(memcmp(&readBuffer[0], &writeBuffer[0], writeOffset) == 0);
+        VerifyOrQuit(memcmp(&readBuffer[writeOffset], &writeBuffer[0], length) == 0);
+    }
+
+    // Test `WriteBytesFromMessage()` behavior copying within same
+    // message at different read and write offsets and lengths.
+
+    for (uint16_t readOffset = 0; readOffset < kMaxSize; readOffset += kOffsetStep)
+    {
+        for (uint16_t writeOffset = 0; writeOffset < kMaxSize; writeOffset += kOffsetStep)
+        {
+            for (uint16_t length = 0; length <= kMaxSize - Max(writeOffset, readOffset); length += kLengthStep)
+            {
+                message->WriteBytes(0, writeBuffer, kMaxSize);
+
+                message->WriteBytesFromMessage(writeOffset, *message, readOffset, length);
+
+                SuccessOrQuit(message->Read(0, readBuffer, kMaxSize));
+
+                VerifyOrQuit(memcmp(&readBuffer[0], writeBuffer, writeOffset) == 0);
+                VerifyOrQuit(memcmp(&readBuffer[writeOffset], &writeBuffer[readOffset], length) == 0);
+                VerifyOrQuit(memcmp(&readBuffer[writeOffset + length], &writeBuffer[writeOffset + length],
+                                    kMaxSize - length - writeOffset) == 0);
+            }
+        }
     }
 
     // Verify `AppendBytesFromMessage()` with two different messages as source and destination.
@@ -236,6 +263,49 @@
     message->Free();
     message2->Free();
 
+    // Verify `RemoveHeader()`
+
+    for (uint16_t offset = 0; offset < kMaxSize; offset += kOffsetStep)
+    {
+        for (uint16_t length = 0; length <= kMaxSize - offset; length += kLengthStep)
+        {
+            VerifyOrQuit((message = messagePool->Allocate(Message::kTypeIp6)) != nullptr);
+            SuccessOrQuit(message->AppendBytes(writeBuffer, kMaxSize));
+
+            message->RemoveHeader(offset, length);
+
+            VerifyOrQuit(message->GetLength() == kMaxSize - length);
+
+            SuccessOrQuit(message->Read(0, readBuffer, kMaxSize - length));
+
+            VerifyOrQuit(memcmp(&readBuffer[0], &writeBuffer[0], offset) == 0);
+            VerifyOrQuit(memcmp(&readBuffer[offset], &writeBuffer[offset + length], kMaxSize - length - offset) == 0);
+            message->Free();
+        }
+    }
+
+    // Verify `InsertHeader()`
+
+    for (uint16_t offset = 0; offset < kMaxSize; offset += kOffsetStep)
+    {
+        for (uint16_t length = 0; length <= kMaxSize; length += kLengthStep)
+        {
+            VerifyOrQuit((message = messagePool->Allocate(Message::kTypeIp6)) != nullptr);
+            SuccessOrQuit(message->AppendBytes(writeBuffer, kMaxSize));
+
+            SuccessOrQuit(message->InsertHeader(offset, length));
+
+            VerifyOrQuit(message->GetLength() == kMaxSize + length);
+
+            SuccessOrQuit(message->Read(0, readBuffer, offset));
+            VerifyOrQuit(memcmp(&readBuffer[0], &writeBuffer[0], offset) == 0);
+
+            SuccessOrQuit(message->Read(offset + length, readBuffer, kMaxSize - offset));
+            VerifyOrQuit(memcmp(&readBuffer[0], &writeBuffer[offset], kMaxSize - offset) == 0);
+            message->Free();
+        }
+    }
+
     testFreeInstance(instance);
 }
 
@@ -246,8 +316,8 @@
 
     static constexpr uint16_t kMaxBufferSize = sizeof(kData1) * 2 + sizeof(kData2);
 
-    Instance *              instance;
-    Message *               message;
+    Instance               *instance;
+    Message                *message;
     uint8_t                 buffer[kMaxBufferSize];
     uint8_t                 zeroBuffer[kMaxBufferSize];
     Appender                bufAppender(buffer, sizeof(buffer));
diff --git a/tests/unit/test_message_queue.cpp b/tests/unit/test_message_queue.cpp
index b8cedc6..9eb3c77 100644
--- a/tests/unit/test_message_queue.cpp
+++ b/tests/unit/test_message_queue.cpp
@@ -40,7 +40,7 @@
 
 #define kNumTestMessages 5
 
-static ot::Instance *   sInstance;
+static ot::Instance    *sInstance;
 static ot::MessagePool *sMessagePool;
 
 // This function verifies the content of the message queue to match the passed in messages
@@ -48,8 +48,8 @@
 {
     const ot::MessageQueue &constQueue = aMessageQueue;
     va_list                 args;
-    ot::Message *           message;
-    ot::Message *           msgArg;
+    ot::Message            *message;
+    ot::Message            *msgArg;
 
     va_start(args, aExpectedLength);
 
@@ -103,7 +103,7 @@
 void TestMessageQueue(void)
 {
     ot::MessageQueue       messageQueue;
-    ot::Message *          messages[kNumTestMessages];
+    ot::Message           *messages[kNumTestMessages];
     ot::MessageQueue::Info info;
 
     sInstance = testInitInstance();
@@ -280,8 +280,8 @@
 // This test checks all the OpenThread C APIs for `otMessageQueue`
 void TestMessageQueueOtApis(void)
 {
-    otMessage *    messages[kNumTestMessages];
-    otMessage *    message;
+    otMessage     *messages[kNumTestMessages];
+    otMessage     *message;
     otMessageQueue queue, queue2;
 
     sInstance = testInitInstance();
diff --git a/tests/unit/test_mle.cpp b/tests/unit/test_mle.cpp
new file mode 100644
index 0000000..e7e1748
--- /dev/null
+++ b/tests/unit/test_mle.cpp
@@ -0,0 +1,185 @@
+/*
+ *  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 "common/num_utils.hpp"
+#include "thread/mle_types.hpp"
+
+namespace ot {
+
+#if OPENTHREAD_FTD
+void TestDefaultDeviceProperties(void)
+{
+    Instance                 *instance;
+    const otDeviceProperties *props;
+    uint8_t                   weight;
+
+    instance = static_cast<Instance *>(testInitInstance());
+    VerifyOrQuit(instance != nullptr);
+
+    props = otThreadGetDeviceProperties(instance);
+
+    VerifyOrQuit(props->mPowerSupply == OPENTHREAD_CONFIG_DEVICE_POWER_SUPPLY);
+    VerifyOrQuit(!props->mSupportsCcm);
+    VerifyOrQuit(!props->mIsUnstable);
+    VerifyOrQuit(props->mLeaderWeightAdjustment == OPENTHREAD_CONFIG_MLE_DEFAULT_LEADER_WEIGHT_ADJUSTMENT);
+#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
+    VerifyOrQuit(props->mIsBorderRouter);
+#else
+    VerifyOrQuit(!props->mIsBorderRouter);
+#endif
+
+    weight = 64;
+
+    switch (props->mPowerSupply)
+    {
+    case OT_POWER_SUPPLY_BATTERY:
+        weight -= 8;
+        break;
+    case OT_POWER_SUPPLY_EXTERNAL:
+        break;
+    case OT_POWER_SUPPLY_EXTERNAL_STABLE:
+        weight += 4;
+        break;
+    case OT_POWER_SUPPLY_EXTERNAL_UNSTABLE:
+        weight -= 4;
+        break;
+    }
+
+    weight += props->mIsBorderRouter ? 1 : 0;
+
+    VerifyOrQuit(otThreadGetLocalLeaderWeight(instance) == weight);
+
+    printf("TestDefaultDeviceProperties passed\n");
+}
+
+void CompareDevicePropertiess(const otDeviceProperties &aFirst, const otDeviceProperties &aSecond)
+{
+    static constexpr int8_t kMinAdjustment = -16;
+    static constexpr int8_t kMaxAdjustment = +16;
+
+    VerifyOrQuit(aFirst.mPowerSupply == aSecond.mPowerSupply);
+    VerifyOrQuit(aFirst.mIsBorderRouter == aSecond.mIsBorderRouter);
+    VerifyOrQuit(aFirst.mSupportsCcm == aSecond.mSupportsCcm);
+    VerifyOrQuit(aFirst.mIsUnstable == aSecond.mIsUnstable);
+    VerifyOrQuit(Clamp(aFirst.mLeaderWeightAdjustment, kMinAdjustment, kMaxAdjustment) ==
+                 Clamp(aSecond.mLeaderWeightAdjustment, kMinAdjustment, kMaxAdjustment));
+}
+
+void TestLeaderWeightCalculation(void)
+{
+    struct TestCase
+    {
+        otDeviceProperties mDeviceProperties;
+        uint8_t            mExpectedLeaderWeight;
+    };
+
+    static const TestCase kTestCases[] = {
+        {{OT_POWER_SUPPLY_BATTERY, false, false, false, 0}, 56},
+        {{OT_POWER_SUPPLY_EXTERNAL, false, false, false, 0}, 64},
+        {{OT_POWER_SUPPLY_EXTERNAL_STABLE, false, false, false, 0}, 68},
+        {{OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, false, false, false, 0}, 60},
+
+        {{OT_POWER_SUPPLY_BATTERY, true, false, false, 0}, 57},
+        {{OT_POWER_SUPPLY_EXTERNAL, true, false, false, 0}, 65},
+        {{OT_POWER_SUPPLY_EXTERNAL_STABLE, true, false, false, 0}, 69},
+        {{OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, true, false, false, 0}, 61},
+
+        {{OT_POWER_SUPPLY_BATTERY, true, true, false, 0}, 64},
+        {{OT_POWER_SUPPLY_EXTERNAL, true, true, false, 0}, 72},
+        {{OT_POWER_SUPPLY_EXTERNAL_STABLE, true, true, false, 0}, 76},
+        {{OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, true, true, false, 0}, 68},
+
+        // Check when `mIsUnstable` is set.
+        {{OT_POWER_SUPPLY_BATTERY, false, false, true, 0}, 56},
+        {{OT_POWER_SUPPLY_EXTERNAL, false, false, true, 0}, 60},
+        {{OT_POWER_SUPPLY_EXTERNAL_STABLE, false, false, true, 0}, 64},
+        {{OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, false, false, true, 0}, 60},
+
+        {{OT_POWER_SUPPLY_BATTERY, true, false, true, 0}, 57},
+        {{OT_POWER_SUPPLY_EXTERNAL, true, false, true, 0}, 61},
+        {{OT_POWER_SUPPLY_EXTERNAL_STABLE, true, false, true, 0}, 65},
+        {{OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, true, false, true, 0}, 61},
+
+        // Include non-zero `mLeaderWeightAdjustment`.
+        {{OT_POWER_SUPPLY_BATTERY, true, false, false, 10}, 67},
+        {{OT_POWER_SUPPLY_EXTERNAL, true, false, false, 10}, 75},
+        {{OT_POWER_SUPPLY_EXTERNAL_STABLE, true, false, false, 10}, 79},
+        {{OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, true, false, false, 10}, 71},
+
+        {{OT_POWER_SUPPLY_BATTERY, false, false, false, -10}, 46},
+        {{OT_POWER_SUPPLY_EXTERNAL, false, false, false, -10}, 54},
+        {{OT_POWER_SUPPLY_EXTERNAL_STABLE, false, false, false, -10}, 58},
+        {{OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, false, false, false, -10}, 50},
+
+        // Use `mLeaderWeightAdjustment` larger than valid range
+        // Make sure it clamps to -16 and +16.
+        {{OT_POWER_SUPPLY_BATTERY, false, false, false, 20}, 72},
+        {{OT_POWER_SUPPLY_EXTERNAL, false, false, false, 20}, 80},
+        {{OT_POWER_SUPPLY_EXTERNAL_STABLE, false, false, false, 20}, 84},
+        {{OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, false, false, false, 20}, 76},
+
+        {{OT_POWER_SUPPLY_BATTERY, true, false, false, -20}, 41},
+        {{OT_POWER_SUPPLY_EXTERNAL, true, false, false, -20}, 49},
+        {{OT_POWER_SUPPLY_EXTERNAL_STABLE, true, false, false, -20}, 53},
+        {{OT_POWER_SUPPLY_EXTERNAL_UNSTABLE, true, false, false, -20}, 45},
+    };
+
+    Instance *instance;
+
+    instance = static_cast<Instance *>(testInitInstance());
+    VerifyOrQuit(instance != nullptr);
+
+    for (const TestCase &testCase : kTestCases)
+    {
+        otThreadSetDeviceProperties(instance, &testCase.mDeviceProperties);
+        CompareDevicePropertiess(testCase.mDeviceProperties, *otThreadGetDeviceProperties(instance));
+        VerifyOrQuit(otThreadGetLocalLeaderWeight(instance) == testCase.mExpectedLeaderWeight);
+    }
+
+    printf("TestLeaderWeightCalculation passed\n");
+}
+
+#endif // OPENTHREAD_FTD
+
+} // namespace ot
+
+int main(void)
+{
+#if OPENTHREAD_FTD
+    ot::TestDefaultDeviceProperties();
+    ot::TestLeaderWeightCalculation();
+#endif
+
+    printf("All tests passed\n");
+    return 0;
+}
diff --git a/tests/unit/test_multicast_listeners_table.cpp b/tests/unit/test_multicast_listeners_table.cpp
index a1e341c..20c810e 100644
--- a/tests/unit/test_multicast_listeners_table.cpp
+++ b/tests/unit/test_multicast_listeners_table.cpp
@@ -57,10 +57,7 @@
 
 uint32_t sNow;
 
-extern "C" uint32_t otPlatAlarmMilliGetNow(void)
-{
-    return sNow;
-}
+extern "C" uint32_t otPlatAlarmMilliGetNow(void) { return sNow; }
 
 void testMulticastListenersTableAPIs(Instance *aInstance);
 
@@ -199,8 +196,5 @@
 }
 
 #else
-int main(void)
-{
-    return 0;
-}
+int main(void) { return 0; }
 #endif // OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE
diff --git a/tests/unit/test_nat64.cpp b/tests/unit/test_nat64.cpp
new file mode 100644
index 0000000..66e5fbe
--- /dev/null
+++ b/tests/unit/test_nat64.cpp
@@ -0,0 +1,314 @@
+/*
+ *  Copyright (c) 2022, 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 "net/nat64_translator.hpp"
+
+#include "test_platform.h"
+#include "test_util.hpp"
+
+#include "string.h"
+
+#include "common/code_utils.hpp"
+#include "common/debug.hpp"
+#include "common/instance.hpp"
+#include "common/message.hpp"
+#include "net/ip6.hpp"
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+
+namespace ot {
+namespace BorderRouter {
+
+static ot::Instance *sInstance;
+
+void DumpMessageInHex(const char *prefix, const uint8_t *aBuf, size_t aBufLen)
+{
+    // This function dumps all packets the output of this function can be imported to packet analyser for debugging.
+    printf("%s", prefix);
+    for (uint16_t i = 0; i < aBufLen; i++)
+    {
+        printf("%02x", aBuf[i]);
+    }
+    printf("\n");
+}
+
+bool CheckMessage(const Message &aMessage, const uint8_t *aExpectedMessage, size_t aExpectedMessageLen)
+{
+    uint8_t  readMessage[OPENTHREAD_CONFIG_IP6_MAX_DATAGRAM_LENGTH];
+    uint16_t messageLength;
+    bool     success = true;
+
+    success       = success && (aMessage.GetLength() == aExpectedMessageLen);
+    messageLength = aMessage.ReadBytes(0, readMessage, aMessage.GetLength());
+    success       = success && (aExpectedMessageLen == messageLength);
+    success       = success && (memcmp(readMessage, aExpectedMessage, aExpectedMessageLen) == 0);
+
+    if (!success)
+    {
+        printf("Expected Message\n");
+        for (uint16_t i = 0; i < aExpectedMessageLen; i++)
+        {
+            printf("%02x%c", aExpectedMessage[i], " \n"[(i & 0xf) == 0xf]);
+        }
+        printf("\n");
+        printf("Actual Message\n");
+        for (uint16_t i = 0; i < messageLength; i++)
+        {
+            printf("%02x%c", readMessage[i], " \n"[(i & 0xf) == 0xf]);
+        }
+        printf("\n");
+    }
+
+    return success;
+}
+
+template <size_t N>
+void TestCase6To4(const char *aTestName,
+                  const uint8_t (&aIp6Message)[N],
+                  Nat64::Translator::Result aResult,
+                  const uint8_t            *aOutMessage,
+                  size_t                    aOutMessageLen)
+{
+    Message *msg = sInstance->Get<Ip6::Ip6>().NewMessage(0);
+
+    printf("Testing NAT64 6 to 4: %s\n", aTestName);
+
+    VerifyOrQuit(msg != nullptr);
+    SuccessOrQuit(msg->AppendBytes(aIp6Message, N));
+
+    DumpMessageInHex("I ", aIp6Message, N);
+
+    VerifyOrQuit(sInstance->Get<Nat64::Translator>().TranslateFromIp6(*msg) == aResult);
+
+    if (aOutMessage != nullptr)
+    {
+        DumpMessageInHex("O ", aOutMessage, aOutMessageLen);
+        VerifyOrQuit(CheckMessage(*msg, aOutMessage, aOutMessageLen));
+    }
+
+    printf("  ... PASS\n");
+}
+
+template <size_t N>
+void TestCase4To6(const char *aTestName,
+                  const uint8_t (&aIp4Message)[N],
+                  Nat64::Translator::Result aResult,
+                  const uint8_t            *aOutMessage,
+                  size_t                    aOutMessageLen)
+{
+    Message *msg = sInstance->Get<Ip6::Ip6>().NewMessage(0);
+
+    printf("Testing NAT64 4 to 6: %s\n", aTestName);
+
+    VerifyOrQuit(msg != nullptr);
+    SuccessOrQuit(msg->AppendBytes(aIp4Message, N));
+
+    DumpMessageInHex("I ", aIp4Message, N);
+
+    VerifyOrQuit(sInstance->Get<Nat64::Translator>().TranslateToIp6(*msg) == aResult);
+
+    if (aOutMessage != nullptr)
+    {
+        DumpMessageInHex("O ", aOutMessage, aOutMessageLen);
+        VerifyOrQuit(CheckMessage(*msg, aOutMessage, aOutMessageLen));
+    }
+
+    printf("  ... PASS\n");
+}
+
+void TestNat64(void)
+{
+    Ip6::Prefix  nat64prefix;
+    Ip4::Cidr    nat64cidr;
+    Ip6::Address ip6Source;
+    Ip6::Address ip6Dest;
+
+    sInstance = testInitInstance();
+
+    {
+        const uint8_t ip6Address[] = {0xfd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+        const uint8_t ip4Address[] = {192, 168, 123, 1};
+
+        nat64cidr.Set(ip4Address, 32);
+        nat64prefix.Set(ip6Address, 96);
+        SuccessOrQuit(sInstance->Get<Nat64::Translator>().SetIp4Cidr(nat64cidr));
+        sInstance->Get<Nat64::Translator>().SetNat64Prefix(nat64prefix);
+    }
+
+    {
+        // fd02::1               fd01::ac10:f3c5       UDP      52     43981 → 4660 Len=4
+        const uint8_t kIp6Packet[] = {
+            0x60, 0x08, 0x6e, 0x38, 0x00, 0x0c, 0x11, 0x40, 0xfd, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            172,  16,   243,  197,  0xab, 0xcd, 0x12, 0x34, 0x00, 0x0c, 0xe3, 0x31, 0x61, 0x62, 0x63, 0x64,
+        };
+        // 192.168.123.1         172.16.243.197        UDP      32     43981 → 4660 Len=4
+        const uint8_t kIp4Packet[] = {0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x11, 0x9f,
+                                      0x4d, 192,  168,  123,  1,    172,  16,   243,  197,  0xab, 0xcd,
+                                      0x12, 0x34, 0x00, 0x0c, 0xa1, 0x8d, 0x61, 0x62, 0x63, 0x64};
+
+        TestCase6To4("good v6 udp datagram", kIp6Packet, Nat64::Translator::kForward, kIp4Packet, sizeof(kIp4Packet));
+    }
+
+    {
+        // 172.16.243.197        192.168.123.1         UDP      32     43981 → 4660 Len=4
+        const uint8_t kIp4Packet[] = {0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x11, 0xa0,
+                                      0x4d, 172,  16,   243,  197,  192,  168,  123,  1,    0xab, 0xcd,
+                                      0x12, 0x34, 0x00, 0x0c, 0xa1, 0x8d, 0x61, 0x62, 0x63, 0x64};
+        // fd01::ac10:f3c5       fd02::1               UDP      52     43981 → 4660 Len=4
+        const uint8_t kIp6Packet[] = {
+            0x60, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x11, 0x3f, 0xfd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 172,  16,   243,  197,  0xfd, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x01, 0xab, 0xcd, 0x12, 0x34, 0x00, 0x0c, 0xe3, 0x31, 0x61, 0x62, 0x63, 0x64,
+        };
+
+        TestCase4To6("good v4 udp datagram", kIp4Packet, Nat64::Translator::kForward, kIp6Packet, sizeof(kIp6Packet));
+    }
+
+    {
+        // fd02::1               fd01::ac10:f3c5       TCP      64     43981 → 4660 [ACK] Seq=1 Ack=1 Win=1 Len=4
+        const uint8_t kIp6Packet[] = {
+            0x60, 0x08, 0x6e, 0x38, 0x00, 0x18, 0x06, 0x40, 0xfd, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 172,  16,   243,  197,  0xab, 0xcd, 0x12, 0x34, 0x87, 0x65, 0x43, 0x21,
+            0x12, 0x34, 0x56, 0x78, 0x50, 0x10, 0x00, 0x01, 0x5f, 0xf8, 0x00, 0x00, 0x61, 0x62, 0x63, 0x64,
+        };
+        // 192.168.123.1         172.16.243.197        TCP      44     43981 → 4660 [ACK] Seq=1 Ack=1 Win=1 Len=4
+        const uint8_t kIp4Packet[] = {0x45, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, 0x9f,
+                                      0x4c, 192,  168,  123,  1,    172,  16,   243,  197,  0xab, 0xcd,
+                                      0x12, 0x34, 0x87, 0x65, 0x43, 0x21, 0x12, 0x34, 0x56, 0x78, 0x50,
+                                      0x10, 0x00, 0x01, 0x1e, 0x54, 0x00, 0x00, 0x61, 0x62, 0x63, 0x64};
+
+        TestCase6To4("good v6 tcp datagram", kIp6Packet, Nat64::Translator::kForward, kIp4Packet, sizeof(kIp4Packet));
+    }
+
+    {
+        // 172.16.243.197        192.168.123.1         TCP      44     43981 → 4660 [ACK] Seq=1 Ack=1 Win=1 Len=4
+        const uint8_t kIp4Packet[] = {0x45, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, 0x9f,
+                                      0x4c, 172,  16,   243,  197,  192,  168,  123,  1,    0xab, 0xcd,
+                                      0x12, 0x34, 0x87, 0x65, 0x43, 0x21, 0x12, 0x34, 0x56, 0x78, 0x50,
+                                      0x10, 0x00, 0x01, 0x1e, 0x54, 0x00, 0x00, 0x61, 0x62, 0x63, 0x64};
+        // fd01::ac10:f3c5       fd02::1               TCP      64     43981 → 4660 [ACK] Seq=1 Ack=1 Win=1 Len=4
+        const uint8_t kIp6Packet[] = {
+            0x60, 0x00, 0x00, 0x00, 0x00, 0x18, 0x06, 0x40, 0xfd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 172,  16,   243,  197,  0xfd, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xab, 0xcd, 0x12, 0x34, 0x87, 0x65, 0x43, 0x21,
+            0x12, 0x34, 0x56, 0x78, 0x50, 0x10, 0x00, 0x01, 0x5f, 0xf8, 0x00, 0x00, 0x61, 0x62, 0x63, 0x64,
+        };
+
+        TestCase4To6("good v4 tcp datagram", kIp4Packet, Nat64::Translator::kForward, kIp6Packet, sizeof(kIp6Packet));
+    }
+
+    {
+        // fd02::1         fd01::ac10:f3c5     ICMPv6   52     Echo (ping) request id=0xaabb, seq=1, hop limit=64
+        const uint8_t kIp6Packet[] = {
+            0x60, 0x08, 0x6e, 0x38, 0x00, 0x0c, 0x3a, 0x40, 0xfd, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            172,  16,   243,  197,  0x80, 0x00, 0x76, 0x59, 0xaa, 0xbb, 0x00, 0x01, 0x61, 0x62, 0x63, 0x64,
+        };
+        // 192.168.123.1   172.16.243.197      ICMP     32     Echo (ping) request  id=0xaabb, seq=1/256, ttl=63
+        const uint8_t kIp4Packet[] = {0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x01, 0x9f,
+                                      0x5d, 192,  168,  123,  1,    172,  16,   243,  197,  0x08, 0x00,
+                                      0x88, 0x7c, 0xaa, 0xbb, 0x00, 0x01, 0x61, 0x62, 0x63, 0x64};
+
+        TestCase6To4("good v6 icmp ping request datagram", kIp6Packet, Nat64::Translator::kForward, kIp4Packet,
+                     sizeof(kIp4Packet));
+    }
+
+    {
+        // 172.16.243.197        192.168.123.1         ICMP     32     Echo (ping) reply    id=0xaabb, seq=1/256, ttl=63
+        const uint8_t kIp4Packet[] = {0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x01, 0xa0,
+                                      0x5d, 172,  16,   243,  197,  192,  168,  123,  1,    0x00, 0x00,
+                                      0x90, 0x7c, 0xaa, 0xbb, 0x00, 0x01, 0x61, 0x62, 0x63, 0x64};
+        // fd01::ac10:f3c5       fd02::1               ICMPv6   52     Echo (ping) reply id=0xaabb, seq=1, hop limit=62
+        const uint8_t kIp6Packet[] = {
+            0x60, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x3a, 0x3f, 0xfd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 172,  16,   243,  197,  0xfd, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x01, 0x81, 0x00, 0x75, 0x59, 0xaa, 0xbb, 0x00, 0x01, 0x61, 0x62, 0x63, 0x64,
+        };
+
+        TestCase4To6("good v4 icmp ping response datagram", kIp4Packet, Nat64::Translator::kForward, kIp6Packet,
+                     sizeof(kIp6Packet));
+    }
+
+    {
+        // fd02::1               N/A                   IPv6     39     Invalid IPv6 header
+        const uint8_t kIp6Packet[] = {0x60, 0x08, 0x6e, 0x38, 0x00, 0x0c, 0x11, 0x40, 0xfd, 0x02, 0x00, 0x00, 0x00,
+                                      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfd, 0x01,
+                                      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 172,  16,   243};
+
+        TestCase6To4("bad v6 datagram", kIp6Packet, Nat64::Translator::kDrop, nullptr, 0);
+    }
+
+    {
+        // 172.16.243.197        N/A                   IPv4     19     [Malformed Packet]
+        const uint8_t kIp4Packet[] = {0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x11,
+                                      0xa0, 0x4c, 172,  16,   243,  197,  192,  168,  123};
+
+        TestCase4To6("bad v4 datagram", kIp4Packet, Nat64::Translator::kDrop, nullptr, 0);
+    }
+
+    {
+        // 172.16.243.197        192.168.123.2         UDP      32     43981 → 4660 Len=4
+        const uint8_t kIp4Packet[] = {0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x11, 0xa0,
+                                      0x4c, 172,  16,   243,  197,  192,  168,  123,  2,    0xab, 0xcd,
+                                      0x12, 0x34, 0x00, 0x0c, 0xa1, 0x8c, 0x61, 0x62, 0x63, 0x64};
+
+        TestCase4To6("no v4 mapping", kIp4Packet, Nat64::Translator::kDrop, nullptr, 0);
+    }
+
+    {
+        // fd02::2               fd01::ac10:f3c5       UDP      52     43981 → 4660 Len=4
+        const uint8_t kIp6Packet[] = {
+            0x60, 0x08, 0x6e, 0x38, 0x00, 0x0c, 0x11, 0x40, 0xfd, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xfd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            172,  16,   243,  197,  0xab, 0xcd, 0x12, 0x34, 0x00, 0x0c, 0xe3, 0x30, 0x61, 0x62, 0x63, 0x64,
+        };
+
+        TestCase6To4("mapping pool exhausted", kIp6Packet, Nat64::Translator::kDrop, nullptr, 0);
+    }
+
+    testFreeInstance(sInstance);
+}
+
+} // namespace BorderRouter
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+
+int main(void)
+{
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    ot::BorderRouter::TestNat64();
+    printf("All tests passed\n");
+#else  // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    printf("NAT64 is not enabled\n");
+#endif // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    return 0;
+}
diff --git a/tests/unit/test_ndproxy_table.cpp b/tests/unit/test_ndproxy_table.cpp
index cee7aec..4589db9 100644
--- a/tests/unit/test_ndproxy_table.cpp
+++ b/tests/unit/test_ndproxy_table.cpp
@@ -111,8 +111,5 @@
 }
 
 #else
-int main(void)
-{
-    return 0;
-}
+int main(void) { return 0; }
 #endif // OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE
diff --git a/tests/unit/test_netif.cpp b/tests/unit/test_netif.cpp
index d82b051..1a5c0e1 100644
--- a/tests/unit/test_netif.cpp
+++ b/tests/unit/test_netif.cpp
@@ -89,7 +89,7 @@
 {
     const uint8_t kMaxAddresses = 8;
 
-    Instance *   instance = testInitInstance();
+    Instance    *instance = testInitInstance();
     TestNetif    netif(*instance);
     Ip6::Address addresses[kMaxAddresses];
 
diff --git a/tests/unit/test_network_data.cpp b/tests/unit/test_network_data.cpp
index 44df236..51c0b67 100644
--- a/tests/unit/test_network_data.cpp
+++ b/tests/unit/test_network_data.cpp
@@ -93,7 +93,7 @@
 
     printf("\nRLOCs: { ");
 
-    for (uint8_t index = 0; index < aRlocsLength; index++)
+    for (uint16_t index = 0; index < aRlocsLength; index++)
     {
         VerifyOrQuit(aRlocs[index] == aExpectedRlocs[index]);
         printf("0x%04x ", aRlocs[index]);
@@ -106,7 +106,7 @@
 {
     static constexpr uint8_t kMaxRlocsArray = 10;
 
-    ot::Instance *      instance;
+    ot::Instance       *instance;
     Iterator            iter = kIteratorInit;
     ExternalRouteConfig rconfig;
     OnMeshPrefixConfig  pconfig;
@@ -639,9 +639,10 @@
 
         struct UnicastEntry
         {
-            const char *                   mAddress;
+            const char                    *mAddress;
             uint16_t                       mPort;
             Service::DnsSrpUnicast::Origin mOrigin;
+            uint16_t                       mRloc16;
 
             bool Matches(Service::DnsSrpUnicast::Info aInfo) const
             {
@@ -650,7 +651,7 @@
                 SuccessOrQuit(sockAddr.GetAddress().FromString(mAddress));
                 sockAddr.SetPort(mPort);
 
-                return (aInfo.mSockAddr == sockAddr) && (aInfo.mOrigin == mOrigin);
+                return (aInfo.mSockAddr == sockAddr) && (aInfo.mOrigin == mOrigin) && (aInfo.mRloc16 == mRloc16);
             }
         };
 
@@ -671,16 +672,16 @@
         };
 
         const UnicastEntry kUnicastEntries[] = {
-            {"fdde:ad00:beef:0:2d0e:c627:5556:18d9", 0x1234, Service::DnsSrpUnicast::kFromServiceData},
-            {"fd00:aabb:ccdd:eeff:11:2233:4455:6677", 0xabcd, Service::DnsSrpUnicast::kFromServerData},
-            {"fdde:ad00:beef:0:0:ff:fe00:2800", 0x5678, Service::DnsSrpUnicast::kFromServerData},
-            {"fd00:1234:5678:9abc:def0:123:4567:89ab", 0x0e, Service::DnsSrpUnicast::kFromServerData},
-            {"fdde:ad00:beef:0:0:ff:fe00:6c00", 0xcd12, Service::DnsSrpUnicast::kFromServerData},
+            {"fdde:ad00:beef:0:2d0e:c627:5556:18d9", 0x1234, Service::DnsSrpUnicast::kFromServiceData, 0xfffe},
+            {"fd00:aabb:ccdd:eeff:11:2233:4455:6677", 0xabcd, Service::DnsSrpUnicast::kFromServerData, 0x6c00},
+            {"fdde:ad00:beef:0:0:ff:fe00:2800", 0x5678, Service::DnsSrpUnicast::kFromServerData, 0x2800},
+            {"fd00:1234:5678:9abc:def0:123:4567:89ab", 0x0e, Service::DnsSrpUnicast::kFromServerData, 0x4c00},
+            {"fdde:ad00:beef:0:0:ff:fe00:6c00", 0xcd12, Service::DnsSrpUnicast::kFromServerData, 0x6c00},
         };
 
         const uint8_t kPreferredAnycastEntryIndex = 2;
 
-        Service::Manager &           manager = instance->Get<Service::Manager>();
+        Service::Manager            &manager = instance->Get<Service::Manager>();
         Service::Manager::Iterator   iterator;
         Service::DnsSrpAnycast::Info anycastInfo;
         Service::DnsSrpUnicast::Info unicastInfo;
@@ -725,8 +726,8 @@
         for (const UnicastEntry &entry : kUnicastEntries)
         {
             SuccessOrQuit(manager.GetNextDnsSrpUnicastInfo(iterator, unicastInfo));
-            printf("\nunicastInfo { %s, origin:%s }", unicastInfo.mSockAddr.ToString().AsCString(),
-                   kOriginStrings[unicastInfo.mOrigin]);
+            printf("\nunicastInfo { %s, origin:%s, rloc16:%04x }", unicastInfo.mSockAddr.ToString().AsCString(),
+                   kOriginStrings[unicastInfo.mOrigin], unicastInfo.mRloc16);
 
             VerifyOrQuit(entry.Matches(unicastInfo), "GetNextDnsSrpUnicastInfo() returned incorrect info");
         }
diff --git a/tests/unit/test_network_name.cpp b/tests/unit/test_network_name.cpp
index c1f448f..874efe1 100644
--- a/tests/unit/test_network_name.cpp
+++ b/tests/unit/test_network_name.cpp
@@ -72,7 +72,8 @@
     SuccessOrQuit(networkName.Set(MeshCoP::NameData(kName2, sizeof(kName2))));
     CompareNetworkName(networkName, kName2);
 
-    VerifyOrQuit(networkName.Set(MeshCoP::NameData(kEmptyName, 0)) == kErrorInvalidArgs);
+    SuccessOrQuit(networkName.Set(MeshCoP::NameData(kEmptyName, 0)));
+    CompareNetworkName(networkName, kEmptyName);
 
     SuccessOrQuit(networkName.Set(MeshCoP::NameData(kLongName, sizeof(kLongName))));
     CompareNetworkName(networkName, kLongName);
@@ -80,8 +81,6 @@
     VerifyOrQuit(networkName.Set(MeshCoP::NameData(kLongName, sizeof(kLongName) - 1)) == kErrorAlready,
                  "failed to detect duplicate");
 
-    VerifyOrQuit(networkName.Set(kEmptyName) == kErrorInvalidArgs);
-
     SuccessOrQuit(networkName.Set(MeshCoP::NameData(kName1, sizeof(kName1))));
 
     VerifyOrQuit(networkName.Set(MeshCoP::NameData(kTooLongName, sizeof(kTooLongName))) == kErrorInvalidArgs,
diff --git a/tests/unit/test_platform.cpp b/tests/unit/test_platform.cpp
index 9d01cd3..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,7 +42,7 @@
     FLASH_SWAP_NUM  = 2,
 };
 
-static uint8_t sFlash[FLASH_SWAP_SIZE * FLASH_SWAP_NUM];
+std::map<uint32_t, std::vector<std::vector<uint8_t>>> settings;
 
 ot::Instance *testInitInstance(void)
 {
@@ -78,28 +83,16 @@
 extern "C" {
 
 #if OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
-OT_TOOL_WEAK void *otPlatCAlloc(size_t aNum, size_t aSize)
-{
-    return calloc(aNum, aSize);
-}
+OT_TOOL_WEAK void *otPlatCAlloc(size_t aNum, size_t aSize) { return calloc(aNum, aSize); }
 
-OT_TOOL_WEAK void otPlatFree(void *aPtr)
-{
-    free(aPtr);
-}
+OT_TOOL_WEAK void otPlatFree(void *aPtr) { free(aPtr); }
 #endif
 
-OT_TOOL_WEAK void otTaskletsSignalPending(otInstance *)
-{
-}
+OT_TOOL_WEAK void otTaskletsSignalPending(otInstance *) {}
 
-OT_TOOL_WEAK void otPlatAlarmMilliStop(otInstance *)
-{
-}
+OT_TOOL_WEAK void otPlatAlarmMilliStop(otInstance *) {}
 
-OT_TOOL_WEAK void otPlatAlarmMilliStartAt(otInstance *, uint32_t, uint32_t)
-{
-}
+OT_TOOL_WEAK void otPlatAlarmMilliStartAt(otInstance *, uint32_t, uint32_t) {}
 
 OT_TOOL_WEAK uint32_t otPlatAlarmMilliGetNow(void)
 {
@@ -110,13 +103,9 @@
     return (uint32_t)((tv.tv_sec * 1000) + (tv.tv_usec / 1000) + 123456);
 }
 
-OT_TOOL_WEAK void otPlatAlarmMicroStop(otInstance *)
-{
-}
+OT_TOOL_WEAK void otPlatAlarmMicroStop(otInstance *) {}
 
-OT_TOOL_WEAK void otPlatAlarmMicroStartAt(otInstance *, uint32_t, uint32_t)
-{
-}
+OT_TOOL_WEAK void otPlatAlarmMicroStartAt(otInstance *, uint32_t, uint32_t) {}
 
 OT_TOOL_WEAK uint32_t otPlatAlarmMicroGetNow(void)
 {
@@ -127,122 +116,55 @@
     return (uint32_t)((tv.tv_sec * 1000000) + tv.tv_usec + 123456);
 }
 
-OT_TOOL_WEAK void otPlatRadioGetIeeeEui64(otInstance *, uint8_t *)
-{
-}
+OT_TOOL_WEAK void otPlatRadioGetIeeeEui64(otInstance *, uint8_t *) {}
 
-OT_TOOL_WEAK void otPlatRadioSetPanId(otInstance *, uint16_t)
-{
-}
+OT_TOOL_WEAK void otPlatRadioSetPanId(otInstance *, uint16_t) {}
 
-OT_TOOL_WEAK void otPlatRadioSetExtendedAddress(otInstance *, const otExtAddress *)
-{
-}
+OT_TOOL_WEAK void otPlatRadioSetExtendedAddress(otInstance *, const otExtAddress *) {}
 
-OT_TOOL_WEAK void otPlatRadioSetShortAddress(otInstance *, uint16_t)
-{
-}
+OT_TOOL_WEAK void otPlatRadioSetShortAddress(otInstance *, uint16_t) {}
 
-OT_TOOL_WEAK void otPlatRadioSetPromiscuous(otInstance *, bool)
-{
-}
+OT_TOOL_WEAK void otPlatRadioSetPromiscuous(otInstance *, bool) {}
 
-OT_TOOL_WEAK bool otPlatRadioIsEnabled(otInstance *)
-{
-    return true;
-}
+OT_TOOL_WEAK bool otPlatRadioIsEnabled(otInstance *) { return true; }
 
-OT_TOOL_WEAK otError otPlatRadioEnable(otInstance *)
-{
-    return OT_ERROR_NONE;
-}
+OT_TOOL_WEAK otError otPlatRadioEnable(otInstance *) { return OT_ERROR_NONE; }
 
-OT_TOOL_WEAK otError otPlatRadioDisable(otInstance *)
-{
-    return OT_ERROR_NONE;
-}
+OT_TOOL_WEAK otError otPlatRadioDisable(otInstance *) { return OT_ERROR_NONE; }
 
-OT_TOOL_WEAK otError otPlatRadioSleep(otInstance *)
-{
-    return OT_ERROR_NONE;
-}
+OT_TOOL_WEAK otError otPlatRadioSleep(otInstance *) { return OT_ERROR_NONE; }
 
-OT_TOOL_WEAK otError otPlatRadioReceive(otInstance *, uint8_t)
-{
-    return OT_ERROR_NONE;
-}
+OT_TOOL_WEAK otError otPlatRadioReceive(otInstance *, uint8_t) { return OT_ERROR_NONE; }
 
-OT_TOOL_WEAK otError otPlatRadioTransmit(otInstance *, otRadioFrame *)
-{
-    return OT_ERROR_NONE;
-}
+OT_TOOL_WEAK otError otPlatRadioTransmit(otInstance *, otRadioFrame *) { return OT_ERROR_NONE; }
 
-OT_TOOL_WEAK otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *)
-{
-    return nullptr;
-}
+OT_TOOL_WEAK otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *) { return nullptr; }
 
-OT_TOOL_WEAK int8_t otPlatRadioGetRssi(otInstance *)
-{
-    return 0;
-}
+OT_TOOL_WEAK int8_t otPlatRadioGetRssi(otInstance *) { return 0; }
 
-OT_TOOL_WEAK otRadioCaps otPlatRadioGetCaps(otInstance *)
-{
-    return OT_RADIO_CAPS_NONE;
-}
+OT_TOOL_WEAK otRadioCaps otPlatRadioGetCaps(otInstance *) { return OT_RADIO_CAPS_NONE; }
 
-OT_TOOL_WEAK bool otPlatRadioGetPromiscuous(otInstance *)
-{
-    return false;
-}
+OT_TOOL_WEAK bool otPlatRadioGetPromiscuous(otInstance *) { return false; }
 
-OT_TOOL_WEAK void otPlatRadioEnableSrcMatch(otInstance *, bool)
-{
-}
+OT_TOOL_WEAK void otPlatRadioEnableSrcMatch(otInstance *, bool) {}
 
-OT_TOOL_WEAK otError otPlatRadioAddSrcMatchShortEntry(otInstance *, uint16_t)
-{
-    return OT_ERROR_NONE;
-}
+OT_TOOL_WEAK otError otPlatRadioAddSrcMatchShortEntry(otInstance *, uint16_t) { return OT_ERROR_NONE; }
 
-OT_TOOL_WEAK otError otPlatRadioAddSrcMatchExtEntry(otInstance *, const otExtAddress *)
-{
-    return OT_ERROR_NONE;
-}
+OT_TOOL_WEAK otError otPlatRadioAddSrcMatchExtEntry(otInstance *, const otExtAddress *) { return OT_ERROR_NONE; }
 
-OT_TOOL_WEAK otError otPlatRadioClearSrcMatchShortEntry(otInstance *, uint16_t)
-{
-    return OT_ERROR_NONE;
-}
+OT_TOOL_WEAK otError otPlatRadioClearSrcMatchShortEntry(otInstance *, uint16_t) { return OT_ERROR_NONE; }
 
-OT_TOOL_WEAK otError otPlatRadioClearSrcMatchExtEntry(otInstance *, const otExtAddress *)
-{
-    return OT_ERROR_NONE;
-}
+OT_TOOL_WEAK otError otPlatRadioClearSrcMatchExtEntry(otInstance *, const otExtAddress *) { return OT_ERROR_NONE; }
 
-OT_TOOL_WEAK void otPlatRadioClearSrcMatchShortEntries(otInstance *)
-{
-}
+OT_TOOL_WEAK void otPlatRadioClearSrcMatchShortEntries(otInstance *) {}
 
-OT_TOOL_WEAK void otPlatRadioClearSrcMatchExtEntries(otInstance *)
-{
-}
+OT_TOOL_WEAK void otPlatRadioClearSrcMatchExtEntries(otInstance *) {}
 
-OT_TOOL_WEAK otError otPlatRadioEnergyScan(otInstance *, uint8_t, uint16_t)
-{
-    return OT_ERROR_NOT_IMPLEMENTED;
-}
+OT_TOOL_WEAK otError otPlatRadioEnergyScan(otInstance *, uint8_t, uint16_t) { return OT_ERROR_NOT_IMPLEMENTED; }
 
-OT_TOOL_WEAK otError otPlatRadioSetTransmitPower(otInstance *, int8_t)
-{
-    return OT_ERROR_NOT_IMPLEMENTED;
-}
+OT_TOOL_WEAK otError otPlatRadioSetTransmitPower(otInstance *, int8_t) { return OT_ERROR_NOT_IMPLEMENTED; }
 
-OT_TOOL_WEAK int8_t otPlatRadioGetReceiveSensitivity(otInstance *)
-{
-    return -100;
-}
+OT_TOOL_WEAK int8_t otPlatRadioGetReceiveSensitivity(otInstance *) { return -100; }
 
 OT_TOOL_WEAK otError otPlatEntropyGet(uint8_t *aOutput, uint16_t aOutputLength)
 {
@@ -252,7 +174,7 @@
 
 #if __SANITIZE_ADDRESS__ == 0
     {
-        FILE * file = nullptr;
+        FILE  *file = nullptr;
         size_t readLength;
 
         file = fopen("/dev/urandom", "rb");
@@ -278,99 +200,127 @@
     return error;
 }
 
-OT_TOOL_WEAK void otPlatDiagProcess(otInstance *, uint8_t, char *aArgs[], char *aOutput, size_t)
+OT_TOOL_WEAK void otPlatDiagProcess(otInstance *, uint8_t, char *aArgs[], char *aOutput, size_t aOutputMaxLen)
 {
-    sprintf(aOutput, "diag feature '%s' is not supported\r\n", aArgs[0]);
+    snprintf(aOutput, aOutputMaxLen, "diag feature '%s' is not supported\r\n", aArgs[0]);
 }
 
-OT_TOOL_WEAK void otPlatDiagModeSet(bool aMode)
-{
-    sDiagMode = aMode;
-}
+OT_TOOL_WEAK void otPlatDiagModeSet(bool aMode) { sDiagMode = aMode; }
 
-OT_TOOL_WEAK bool otPlatDiagModeGet()
-{
-    return sDiagMode;
-}
+OT_TOOL_WEAK bool otPlatDiagModeGet() { return sDiagMode; }
 
-OT_TOOL_WEAK void otPlatDiagChannelSet(uint8_t)
-{
-}
+OT_TOOL_WEAK void otPlatDiagChannelSet(uint8_t) {}
 
-OT_TOOL_WEAK void otPlatDiagTxPowerSet(int8_t)
-{
-}
+OT_TOOL_WEAK void otPlatDiagTxPowerSet(int8_t) {}
 
-OT_TOOL_WEAK void otPlatDiagRadioReceived(otInstance *, otRadioFrame *, otError)
-{
-}
+OT_TOOL_WEAK void otPlatDiagRadioReceived(otInstance *, otRadioFrame *, otError) {}
 
-OT_TOOL_WEAK void otPlatDiagAlarmCallback(otInstance *)
-{
-}
+OT_TOOL_WEAK void otPlatDiagAlarmCallback(otInstance *) {}
 
-OT_TOOL_WEAK void otPlatUartSendDone(void)
-{
-}
+OT_TOOL_WEAK void otPlatUartSendDone(void) {}
 
-OT_TOOL_WEAK void otPlatUartReceived(const uint8_t *, uint16_t)
-{
-}
+OT_TOOL_WEAK void otPlatUartReceived(const uint8_t *, uint16_t) {}
 
-OT_TOOL_WEAK void otPlatReset(otInstance *)
-{
-}
+OT_TOOL_WEAK void otPlatReset(otInstance *) {}
 
-OT_TOOL_WEAK otPlatResetReason otPlatGetResetReason(otInstance *)
-{
-    return OT_PLAT_RESET_REASON_POWER_ON;
-}
+OT_TOOL_WEAK otPlatResetReason otPlatGetResetReason(otInstance *) { return OT_PLAT_RESET_REASON_POWER_ON; }
 
-OT_TOOL_WEAK void otPlatLog(otLogLevel, otLogRegion, const char *, ...)
-{
-}
+OT_TOOL_WEAK void otPlatLog(otLogLevel, otLogRegion, const char *, ...) {}
 
-OT_TOOL_WEAK void otPlatSettingsInit(otInstance *, const uint16_t *, uint16_t)
-{
-}
+OT_TOOL_WEAK void otPlatSettingsInit(otInstance *, const uint16_t *, uint16_t) {}
 
-OT_TOOL_WEAK void otPlatSettingsDeinit(otInstance *)
-{
-}
+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);
 
-OT_TOOL_WEAK otError otPlatSettingsSet(otInstance *, uint16_t, const uint8_t *, uint16_t)
-{
+    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 otPlatSettingsAdd(otInstance *, uint16_t, const uint8_t *, uint16_t)
+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);
+
+    settings[aKey].clear();
+    settings[aKey].push_back(setting);
+
     return OT_ERROR_NONE;
 }
 
-OT_TOOL_WEAK otError otPlatSettingsDelete(otInstance *, uint16_t, int)
+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 void otPlatSettingsWipe(otInstance *)
+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 otPlatFlashInit(otInstance *)
+OT_TOOL_WEAK void otPlatSettingsWipe(otInstance *) { settings.clear(); }
+
+uint8_t *GetFlash(void)
 {
-    memset(sFlash, 0xff, sizeof(sFlash));
+    static uint8_t sFlash[FLASH_SWAP_SIZE * FLASH_SWAP_NUM];
+    static bool    sInitialized;
+
+    if (!sInitialized)
+    {
+        memset(sFlash, 0xff, sizeof(sFlash));
+        sInitialized = true;
+    }
+
+    return sFlash;
 }
 
-OT_TOOL_WEAK uint32_t otPlatFlashGetSwapSize(otInstance *)
-{
-    return FLASH_SWAP_SIZE;
-}
+OT_TOOL_WEAK void otPlatFlashInit(otInstance *) {}
+
+OT_TOOL_WEAK uint32_t otPlatFlashGetSwapSize(otInstance *) { return FLASH_SWAP_SIZE; }
 
 OT_TOOL_WEAK void otPlatFlashErase(otInstance *, uint8_t aSwapIndex)
 {
@@ -380,7 +330,7 @@
 
     address = aSwapIndex ? FLASH_SWAP_SIZE : 0;
 
-    memset(sFlash + address, 0xff, FLASH_SWAP_SIZE);
+    memset(GetFlash() + address, 0xff, FLASH_SWAP_SIZE);
 }
 
 OT_TOOL_WEAK void otPlatFlashRead(otInstance *, uint8_t aSwapIndex, uint32_t aOffset, void *aData, uint32_t aSize)
@@ -393,7 +343,7 @@
 
     address = aSwapIndex ? FLASH_SWAP_SIZE : 0;
 
-    memcpy(aData, sFlash + address + aOffset, aSize);
+    memcpy(aData, GetFlash() + address + aOffset, aSize);
 }
 
 OT_TOOL_WEAK void otPlatFlashWrite(otInstance *,
@@ -412,15 +362,12 @@
 
     for (uint32_t index = 0; index < aSize; index++)
     {
-        sFlash[address + aOffset + index] &= ((uint8_t *)aData)[index];
+        GetFlash()[address + aOffset + index] &= ((uint8_t *)aData)[index];
     }
 }
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE || OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-OT_TOOL_WEAK uint16_t otPlatTimeGetXtalAccuracy(void)
-{
-    return 0;
-}
+OT_TOOL_WEAK uint16_t otPlatTimeGetXtalAccuracy(void) { return 0; }
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
@@ -429,9 +376,7 @@
     return OT_ERROR_NONE;
 }
 
-OT_TOOL_WEAK void otPlatRadioUpdateCslSampleTime(otInstance *, uint32_t)
-{
-}
+OT_TOOL_WEAK void otPlatRadioUpdateCslSampleTime(otInstance *, uint32_t) {}
 
 OT_TOOL_WEAK uint8_t otPlatRadioGetCslAccuracy(otInstance *)
 {
@@ -440,27 +385,17 @@
 #endif
 
 #if OPENTHREAD_CONFIG_OTNS_ENABLE
-OT_TOOL_WEAK void otPlatOtnsStatus(const char *)
-{
-}
+OT_TOOL_WEAK void otPlatOtnsStatus(const char *) {}
 #endif
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
-OT_TOOL_WEAK void otPlatTrelEnable(otInstance *, uint16_t *)
-{
-}
+OT_TOOL_WEAK void otPlatTrelEnable(otInstance *, uint16_t *) {}
 
-OT_TOOL_WEAK void otPlatTrelDisable(otInstance *)
-{
-}
+OT_TOOL_WEAK void otPlatTrelDisable(otInstance *) {}
 
-OT_TOOL_WEAK void otPlatTrelSend(otInstance *, const uint8_t *, uint16_t, const otSockAddr *)
-{
-}
+OT_TOOL_WEAK void otPlatTrelSend(otInstance *, const uint8_t *, uint16_t, const otSockAddr *) {}
 
-OT_TOOL_WEAK void otPlatTrelRegisterService(otInstance *, uint16_t, const uint8_t *, uint8_t)
-{
-}
+OT_TOOL_WEAK void otPlatTrelRegisterService(otInstance *, uint16_t, const uint8_t *, uint8_t) {}
 #endif
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
@@ -483,25 +418,24 @@
 #endif
 
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
-OT_TOOL_WEAK bool otPlatInfraIfHasAddress(uint32_t, const otIp6Address *)
-{
-    return false;
-}
+OT_TOOL_WEAK bool otPlatInfraIfHasAddress(uint32_t, const otIp6Address *) { return false; }
 
 OT_TOOL_WEAK otError otPlatInfraIfSendIcmp6Nd(uint32_t, const otIp6Address *, const uint8_t *, uint16_t)
 {
     return OT_ERROR_FAILED;
 }
+
+OT_TOOL_WEAK otError otPlatInfraIfDiscoverNat64Prefix(uint32_t) { return OT_ERROR_FAILED; }
 #endif
 
 #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
 
-otError otPlatCryptoImportKey(otCryptoKeyRef *     aKeyRef,
+otError otPlatCryptoImportKey(otCryptoKeyRef      *aKeyRef,
                               otCryptoKeyType      aKeyType,
                               otCryptoKeyAlgorithm aKeyAlgorithm,
                               int                  aKeyUsage,
                               otCryptoKeyStorage   aKeyPersistence,
-                              const uint8_t *      aKey,
+                              const uint8_t       *aKey,
                               size_t               aKeyLen)
 {
     OT_UNUSED_VARIABLE(aKeyRef);
@@ -542,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);
@@ -578,4 +549,78 @@
 
 #endif // #if OPENTHREAD_CONFIG_DNS_DSO_ENABLE
 
+#if OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE
+otError otPlatUdpSocket(otUdpSocket *aUdpSocket)
+{
+    OT_UNUSED_VARIABLE(aUdpSocket);
+    return OT_ERROR_NONE;
+}
+
+otError otPlatUdpClose(otUdpSocket *aUdpSocket)
+{
+    OT_UNUSED_VARIABLE(aUdpSocket);
+    return OT_ERROR_NONE;
+}
+
+otError otPlatUdpBind(otUdpSocket *aUdpSocket)
+{
+    OT_UNUSED_VARIABLE(aUdpSocket);
+    return OT_ERROR_NONE;
+}
+
+otError otPlatUdpBindToNetif(otUdpSocket *aUdpSocket, otNetifIdentifier aNetifIdentifier)
+{
+    OT_UNUSED_VARIABLE(aUdpSocket);
+    OT_UNUSED_VARIABLE(aNetifIdentifier);
+    return OT_ERROR_NONE;
+}
+
+otError otPlatUdpConnect(otUdpSocket *aUdpSocket)
+{
+    OT_UNUSED_VARIABLE(aUdpSocket);
+    return OT_ERROR_NONE;
+}
+
+otError otPlatUdpSend(otUdpSocket *aUdpSocket, otMessage *aMessage, const otMessageInfo *aMessageInfo)
+{
+    OT_UNUSED_VARIABLE(aUdpSocket);
+    OT_UNUSED_VARIABLE(aMessageInfo);
+    return OT_ERROR_NONE;
+}
+
+otError otPlatUdpJoinMulticastGroup(otUdpSocket        *aUdpSocket,
+                                    otNetifIdentifier   aNetifIdentifier,
+                                    const otIp6Address *aAddress)
+{
+    OT_UNUSED_VARIABLE(aUdpSocket);
+    OT_UNUSED_VARIABLE(aNetifIdentifier);
+    OT_UNUSED_VARIABLE(aAddress);
+    return OT_ERROR_NONE;
+}
+
+otError otPlatUdpLeaveMulticastGroup(otUdpSocket        *aUdpSocket,
+                                     otNetifIdentifier   aNetifIdentifier,
+                                     const otIp6Address *aAddress)
+{
+    OT_UNUSED_VARIABLE(aUdpSocket);
+    OT_UNUSED_VARIABLE(aNetifIdentifier);
+    OT_UNUSED_VARIABLE(aAddress);
+    return OT_ERROR_NONE;
+}
+#endif // OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE
+
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+void otPlatDnsStartUpstreamQuery(otInstance *aInstance, otPlatDnsUpstreamQuery *aTxn, const otMessage *aQuery)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aTxn);
+    OT_UNUSED_VARIABLE(aQuery);
+}
+
+void otPlatDnsCancelUpstreamQuery(otInstance *aInstance, otPlatDnsUpstreamQuery *aTxn)
+{
+    otPlatDnsUpstreamQueryDone(aInstance, aTxn, nullptr);
+}
+#endif
+
 } // extern "C"
diff --git a/tests/unit/test_platform.h b/tests/unit/test_platform.h
index 2944e52..ec5f887 100644
--- a/tests/unit/test_platform.h
+++ b/tests/unit/test_platform.h
@@ -33,6 +33,7 @@
 
 #include <openthread/config.h>
 #include <openthread/platform/alarm-milli.h>
+#include <openthread/platform/dns.h>
 #include <openthread/platform/dso_transport.h>
 #include <openthread/platform/entropy.h>
 #include <openthread/platform/logging.h>
diff --git a/tests/unit/test_power_calibration.cpp b/tests/unit/test_power_calibration.cpp
new file mode 100644
index 0000000..91c1d69
--- /dev/null
+++ b/tests/unit/test_power_calibration.cpp
@@ -0,0 +1,151 @@
+/*
+ *  Copyright (c) 2022, 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/platform/radio.h>
+
+#include "test_platform.h"
+#include "test_util.h"
+
+#if OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+
+namespace ot {
+
+void TestPowerCalibration(void)
+{
+    otInstance *instance;
+    uint8_t     rawPowerSetting[2];
+    uint16_t    rawPowerSettingLength;
+
+    struct CalibratedPowerEntry
+    {
+        uint8_t  mChannel;
+        int16_t  mActualPower;
+        uint8_t  mRawPowerSetting[1];
+        uint16_t mRawPowerSettingLength;
+    };
+
+    constexpr CalibratedPowerEntry kCalibratedPowerTable[] = {
+        {11, 15000, {0x02}, 1},
+        {11, 5000, {0x00}, 1},
+        {11, 10000, {0x01}, 1},
+    };
+
+    instance = static_cast<otInstance *>(testInitInstance());
+    VerifyOrQuit(instance != nullptr, "Null OpenThread instance");
+
+    for (const CalibratedPowerEntry &calibratedPower : kCalibratedPowerTable)
+    {
+        SuccessOrQuit(otPlatRadioAddCalibratedPower(instance, calibratedPower.mChannel, calibratedPower.mActualPower,
+                                                    calibratedPower.mRawPowerSetting,
+                                                    calibratedPower.mRawPowerSettingLength));
+    }
+
+    SuccessOrQuit(otPlatRadioSetChannelTargetPower(instance, 11, 4999));
+    rawPowerSettingLength = sizeof(rawPowerSetting);
+    VerifyOrQuit(otPlatRadioGetRawPowerSetting(instance, 11, rawPowerSetting, &rawPowerSettingLength) ==
+                 OT_ERROR_NOT_FOUND);
+
+    SuccessOrQuit(otPlatRadioSetChannelTargetPower(instance, 11, 5000));
+    rawPowerSettingLength = sizeof(rawPowerSetting);
+    SuccessOrQuit(otPlatRadioGetRawPowerSetting(instance, 11, rawPowerSetting, &rawPowerSettingLength));
+    VerifyOrQuit(rawPowerSettingLength == 1);
+    VerifyOrQuit(rawPowerSetting[0] == 0x00);
+
+    SuccessOrQuit(otPlatRadioSetChannelTargetPower(instance, 11, 9999));
+    rawPowerSettingLength = sizeof(rawPowerSetting);
+    SuccessOrQuit(otPlatRadioGetRawPowerSetting(instance, 11, rawPowerSetting, &rawPowerSettingLength));
+    VerifyOrQuit(rawPowerSettingLength == 1);
+    VerifyOrQuit(rawPowerSetting[0] == 0x00);
+
+    SuccessOrQuit(otPlatRadioSetChannelTargetPower(instance, 11, 10000));
+    rawPowerSettingLength = sizeof(rawPowerSetting);
+    SuccessOrQuit(otPlatRadioGetRawPowerSetting(instance, 11, rawPowerSetting, &rawPowerSettingLength));
+    VerifyOrQuit(rawPowerSettingLength == 1);
+    VerifyOrQuit(rawPowerSetting[0] == 0x01);
+
+    SuccessOrQuit(otPlatRadioSetChannelTargetPower(instance, 11, 14999));
+    rawPowerSettingLength = sizeof(rawPowerSetting);
+    SuccessOrQuit(otPlatRadioGetRawPowerSetting(instance, 11, rawPowerSetting, &rawPowerSettingLength));
+    VerifyOrQuit(rawPowerSettingLength == 1);
+    VerifyOrQuit(rawPowerSetting[0] == 0x01);
+
+    SuccessOrQuit(otPlatRadioSetChannelTargetPower(instance, 11, 15000));
+    rawPowerSettingLength = sizeof(rawPowerSetting);
+    SuccessOrQuit(otPlatRadioGetRawPowerSetting(instance, 11, rawPowerSetting, &rawPowerSettingLength));
+    VerifyOrQuit(rawPowerSettingLength == 1);
+    VerifyOrQuit(rawPowerSetting[0] == 0x02);
+
+    SuccessOrQuit(otPlatRadioSetChannelTargetPower(instance, 11, 15001));
+    rawPowerSettingLength = sizeof(rawPowerSetting);
+    SuccessOrQuit(otPlatRadioGetRawPowerSetting(instance, 11, rawPowerSetting, &rawPowerSettingLength));
+    VerifyOrQuit(rawPowerSettingLength == 1);
+    VerifyOrQuit(rawPowerSetting[0] == 0x02);
+
+    rawPowerSettingLength = sizeof(rawPowerSetting);
+    VerifyOrQuit(otPlatRadioGetRawPowerSetting(instance, 12, rawPowerSetting, &rawPowerSettingLength) ==
+                 OT_ERROR_NOT_FOUND);
+
+    SuccessOrQuit(otPlatRadioClearCalibratedPowers(instance));
+    rawPowerSettingLength = sizeof(rawPowerSetting);
+    VerifyOrQuit(otPlatRadioGetRawPowerSetting(instance, 11, rawPowerSetting, &rawPowerSettingLength) ==
+                 OT_ERROR_NOT_FOUND);
+
+    for (const CalibratedPowerEntry &calibratedPower : kCalibratedPowerTable)
+    {
+        SuccessOrQuit(otPlatRadioAddCalibratedPower(instance, calibratedPower.mChannel, calibratedPower.mActualPower,
+                                                    calibratedPower.mRawPowerSetting,
+                                                    calibratedPower.mRawPowerSettingLength));
+    }
+
+    SuccessOrQuit(otPlatRadioSetChannelTargetPower(instance, 11, 15000));
+    rawPowerSettingLength = sizeof(rawPowerSetting);
+    SuccessOrQuit(otPlatRadioGetRawPowerSetting(instance, 11, rawPowerSetting, &rawPowerSettingLength));
+    VerifyOrQuit(rawPowerSettingLength == 1);
+    VerifyOrQuit(rawPowerSetting[0] == 0x02);
+
+    VerifyOrQuit(
+        otPlatRadioAddCalibratedPower(instance, kCalibratedPowerTable[0].mChannel,
+                                      kCalibratedPowerTable[0].mActualPower, kCalibratedPowerTable[0].mRawPowerSetting,
+                                      kCalibratedPowerTable[0].mRawPowerSettingLength) == OT_ERROR_INVALID_ARGS);
+
+    testFreeInstance(instance);
+}
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+
+int main(void)
+{
+#if OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+    ot::TestPowerCalibration();
+    printf("All tests passed\n");
+#else  // OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+    printf("Power calibration is not enabled\n");
+#endif // OPENTHREAD_CONFIG_POWER_CALIBRATION_ENABLE && OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
+    return 0;
+}
diff --git a/tests/unit/test_priority_queue.cpp b/tests/unit/test_priority_queue.cpp
index 51f6b72..6348f55 100644
--- a/tests/unit/test_priority_queue.cpp
+++ b/tests/unit/test_priority_queue.cpp
@@ -44,8 +44,8 @@
 {
     const ot::PriorityQueue &constQueue = aPriorityQueue;
     va_list                  args;
-    ot::Message *            message;
-    ot::Message *            msgArg;
+    ot::Message             *message;
+    ot::Message             *msgArg;
     int8_t                   curPriority = ot::Message::kNumPriorities;
     ot::PriorityQueue::Info  info;
 
@@ -168,14 +168,14 @@
 
 void TestPriorityQueue(void)
 {
-    ot::Instance *    instance;
-    ot::MessagePool * messagePool;
+    ot::Instance     *instance;
+    ot::MessagePool  *messagePool;
     ot::PriorityQueue queue;
     ot::MessageQueue  messageQueue;
-    ot::Message *     msgNet[kNumTestMessages];
-    ot::Message *     msgHigh[kNumTestMessages];
-    ot::Message *     msgNor[kNumTestMessages];
-    ot::Message *     msgLow[kNumTestMessages];
+    ot::Message      *msgNet[kNumTestMessages];
+    ot::Message      *msgHigh[kNumTestMessages];
+    ot::Message      *msgNor[kNumTestMessages];
+    ot::Message      *msgLow[kNumTestMessages];
 
     instance = testInitInstance();
     VerifyOrQuit(instance != nullptr, "Null OpenThread instance");
diff --git a/tests/unit/test_pskc.cpp b/tests/unit/test_pskc.cpp
index 3eedad9..0ae4e97 100644
--- a/tests/unit/test_pskc.cpp
+++ b/tests/unit/test_pskc.cpp
@@ -39,10 +39,10 @@
 {
     ot::Pskc              pskc;
     const uint8_t         expectedPskc[] = {0x44, 0x98, 0x8e, 0x22, 0xcf, 0x65, 0x2e, 0xee,
-                                    0xcc, 0xd1, 0xe4, 0xc0, 0x1d, 0x01, 0x54, 0xf8};
+                                            0xcc, 0xd1, 0xe4, 0xc0, 0x1d, 0x01, 0x54, 0xf8};
     const otExtendedPanId xpanid         = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}};
     const char            passphrase[]   = "123456";
-    otInstance *          instance       = testInitInstance();
+    otInstance           *instance       = testInitInstance();
     SuccessOrQuit(ot::MeshCoP::GeneratePskc(passphrase,
                                             *reinterpret_cast<const ot::MeshCoP::NetworkName *>("OpenThread"),
                                             static_cast<const ot::MeshCoP::ExtendedPanId &>(xpanid), pskc));
@@ -54,24 +54,24 @@
 {
     ot::Pskc              pskc;
     const uint8_t         expectedPskc[] = {0x9e, 0x81, 0xbd, 0x35, 0xa2, 0x53, 0x76, 0x2f,
-                                    0x80, 0xee, 0x04, 0xff, 0x2f, 0xa2, 0x85, 0xe9};
+                                            0x80, 0xee, 0x04, 0xff, 0x2f, 0xa2, 0x85, 0xe9};
     const otExtendedPanId xpanid         = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}};
     const char            passphrase[]   = "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "1234567812345678"
-                              "123456781234567";
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "1234567812345678"
+                                           "123456781234567";
 
     otInstance *instance = testInitInstance();
     SuccessOrQuit(ot::MeshCoP::GeneratePskc(passphrase,
@@ -85,7 +85,7 @@
 {
     ot::Pskc              pskc;
     const uint8_t         expectedPskc[] = {0xc3, 0xf5, 0x93, 0x68, 0x44, 0x5a, 0x1b, 0x61,
-                                    0x06, 0xbe, 0x42, 0x0a, 0x70, 0x6d, 0x4c, 0xc9};
+                                            0x06, 0xbe, 0x42, 0x0a, 0x70, 0x6d, 0x4c, 0xc9};
     const otExtendedPanId xpanid         = {{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}};
     const char            passphrase[]   = "12SECRETPASSWORD34";
 
diff --git a/tests/unit/test_routing_manager.cpp b/tests/unit/test_routing_manager.cpp
index 56085d0..bd1174b 100644
--- a/tests/unit/test_routing_manager.cpp
+++ b/tests/unit/test_routing_manager.cpp
@@ -35,7 +35,9 @@
 
 #include "border_router/routing_manager.hpp"
 #include "common/arg_macros.hpp"
+#include "common/array.hpp"
 #include "common/instance.hpp"
+#include "common/time.hpp"
 #include "net/icmp6.hpp"
 #include "net/nd6.hpp"
 
@@ -44,8 +46,8 @@
 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, \
+#define Log(...)                                                                                         \
+    printf("%02u:%02u:%02u.%03u " OT_FIRST_ARG(__VA_ARGS__) "\n", (sNow / 3600000), (sNow / 60000) % 60, \
            (sNow / 1000) % 60, sNow % 1000 OT_REST_ARGS(__VA_ARGS__))
 
 static constexpr uint32_t kInfraIfIndex     = 1;
@@ -54,7 +56,10 @@
 static constexpr uint32_t kValidLitime       = 2000;
 static constexpr uint32_t kPreferredLifetime = 1800;
 
-static otInstance *sInstance;
+static constexpr uint16_t kMaxRaSize              = 800;
+static constexpr uint16_t kMaxDeprecatingPrefixes = 16;
+
+static ot::Instance *sInstance;
 
 static uint32_t sNow = 0;
 static uint32_t sAlarmTime;
@@ -73,13 +78,75 @@
     kPioDeprecatingLocalOnLink, // Expect to see local on-link prefix deprecated (zero preferred lifetime).
 };
 
+struct DeprecatingPrefix
+{
+    DeprecatingPrefix(void) = default;
+
+    DeprecatingPrefix(const Ip6::Prefix &aPrefix, uint32_t aLifetime)
+        : mPrefix(aPrefix)
+        , mLifetime(aLifetime)
+    {
+    }
+
+    bool Matches(const Ip6::Prefix &aPrefix) const { return mPrefix == aPrefix; }
+
+    Ip6::Prefix mPrefix;   // Old on-link prefix being deprecated.
+    uint32_t    mLifetime; // Valid lifetime of prefix from PIO.
+};
+
 static Ip6::Address sInfraIfAddress;
 
-bool        sRsEmitted;         // Indicates if an RS message was emitted by BR.
-bool        sRaValidated;       // Indicates if an RA was emitted by BR and successfully validated.
-bool        sSawExpectedRio;    // Indicates if the emitted RA by BR contained an RIO with `sExpectedRioPrefix`
-ExpectedPio sExpectedPio;       // Expected PIO in the emitted RA by BR (MUST be seen in RA to set `sRaValidated`).
-Ip6::Prefix sExpectedRioPrefix; // Expected RIO prefix to see in RA (MUST be seen to set `sSawExpectedRio`).
+bool        sRsEmitted;      // Indicates if an RS message was emitted by BR.
+bool        sRaValidated;    // Indicates if an RA was emitted by BR and successfully validated.
+bool        sNsEmitted;      // Indicates if an NS message was emitted by BR.
+bool        sRespondToNs;    // Indicates whether or not to respond to NS.
+ExpectedPio sExpectedPio;    // Expected PIO in the emitted RA by BR (MUST be seen in RA to set `sRaValidated`).
+uint32_t    sOnLinkLifetime; // Valid lifetime for local on-link prefix from the last processed RA.
+
+// Array containing deprecating prefixes from PIOs in the last processed RA.
+Array<DeprecatingPrefix, kMaxDeprecatingPrefixes> sDeprecatingPrefixes;
+
+static constexpr uint16_t kMaxRioPrefixes = 10;
+
+struct RioPrefix
+{
+    RioPrefix(void) = default;
+
+    explicit RioPrefix(const Ip6::Prefix &aPrefix)
+        : mSawInRa(false)
+        , mPrefix(aPrefix)
+        , mLifetime(0)
+    {
+    }
+
+    bool        mSawInRa;  // Indicate whether or not this prefix was seen in the emitted RA (as RIO).
+    Ip6::Prefix mPrefix;   // The RIO prefix.
+    uint32_t    mLifetime; // The RIO prefix lifetime - only valid when `mSawInRa`
+};
+
+class ExpectedRios : public Array<RioPrefix, kMaxRioPrefixes>
+{
+public:
+    void Add(const Ip6::Prefix &aPrefix) { SuccessOrQuit(PushBack(RioPrefix(aPrefix))); }
+
+    bool SawAll(void) const
+    {
+        bool sawAll = true;
+
+        for (const RioPrefix &rioPrefix : *this)
+        {
+            if (!rioPrefix.mSawInRa)
+            {
+                sawAll = false;
+                break;
+            }
+        }
+
+        return sawAll;
+    }
+};
+
+ExpectedRios sExpectedRios; // Expected RIO prefixes in emitted RAs.
 
 //----------------------------------------------------------------------------------------------------------------------
 // Function prototypes
@@ -88,8 +155,25 @@
 void        AdvanceTime(uint32_t aDuration);
 void        LogRouterAdvert(const Icmp6Packet &aPacket);
 void        ValidateRouterAdvert(const Icmp6Packet &aPacket);
-const char *PreferenceToString(uint8_t aPreference);
+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);
+
+#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
 
 //----------------------------------------------------------------------------------------------------------------------
 // `otPlatRadio
@@ -103,18 +187,12 @@
     return OT_ERROR_NONE;
 }
 
-otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *)
-{
-    return &sRadioTxFrame;
-}
+otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *) { return &sRadioTxFrame; }
 
 //----------------------------------------------------------------------------------------------------------------------
 // `otPlatAlaram
 
-void otPlatAlarmMilliStop(otInstance *)
-{
-    sAlarmOn = false;
-}
+void otPlatAlarmMilliStop(otInstance *) { sAlarmOn = false; }
 
 void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt)
 {
@@ -122,10 +200,7 @@
     sAlarmTime = aT0 + aDt;
 }
 
-uint32_t otPlatAlarmMilliGetNow(void)
-{
-    return sNow;
-}
+uint32_t otPlatAlarmMilliGetNow(void) { return sNow; }
 
 //---------------------------------------------------------------------------------------------------------------------
 // otPlatInfraIf
@@ -139,7 +214,7 @@
 
 otError otPlatInfraIfSendIcmp6Nd(uint32_t            aInfraIfIndex,
                                  const otIp6Address *aDestAddress,
-                                 const uint8_t *     aBuffer,
+                                 const uint8_t      *aBuffer,
                                  uint16_t            aBufferLength)
 {
     Icmp6Packet packet;
@@ -164,9 +239,32 @@
         Log("  Router Advertisement message");
         LogRouterAdvert(packet);
         ValidateRouterAdvert(packet);
-        sRaValidated = true;
         break;
 
+    case Ip6::Icmp::Header::kTypeNeighborSolicit:
+    {
+        const Ip6::Nd::NeighborSolicitMessage *nsMsg =
+            reinterpret_cast<const Ip6::Nd::NeighborSolicitMessage *>(packet.GetBytes());
+
+        Log("  Neighbor Solicit message");
+
+        VerifyOrQuit(packet.GetLength() >= sizeof(Ip6::Nd::NeighborSolicitMessage));
+        VerifyOrQuit(nsMsg->IsValid());
+        sNsEmitted = true;
+
+        if (sRespondToNs)
+        {
+            Ip6::Nd::NeighborAdvertMessage naMsg;
+
+            naMsg.SetTargetAddress(nsMsg->GetTargetAddress());
+            naMsg.SetRouterFlag();
+            naMsg.SetSolicitedFlag();
+            SendNeighborAdvert(AsCoreType(aDestAddress), naMsg);
+        }
+
+        break;
+    }
+
     default:
         VerifyOrQuit(false, "Bad ICMP6 type");
     }
@@ -199,7 +297,7 @@
 
     Log("AdvanceTime for %u.%03u", aDuration / 1000, aDuration % 1000);
 
-    while (sAlarmTime <= time)
+    while (TimeMilli(sAlarmTime) <= TimeMilli(time))
     {
         ProcessRadioTxAndTasklets();
         sNow = sAlarmTime;
@@ -212,10 +310,17 @@
 
 void ValidateRouterAdvert(const Icmp6Packet &aPacket)
 {
-    Ip6::Nd::RouterAdvertMessage raMsg(aPacket);
+    constexpr uint8_t kMaxPrefixes = 16;
+
+    Ip6::Nd::RouterAdvertMessage     raMsg(aPacket);
+    bool                             sawExpectedPio = false;
+    Array<Ip6::Prefix, kMaxPrefixes> pioPrefixes;
+    Array<Ip6::Prefix, kMaxPrefixes> rioPrefixes;
 
     VerifyOrQuit(raMsg.IsValid());
 
+    sDeprecatingPrefixes.Clear();
+
     for (const Ip6::Nd::Option &option : raMsg)
     {
         switch (option.GetType())
@@ -229,20 +334,40 @@
             VerifyOrQuit(pio.IsValid());
             pio.GetPrefix(prefix);
 
-            VerifyOrQuit(sExpectedPio != kNoPio, "Received RA contain an unexpected PIO");
+            VerifyOrQuit(!pioPrefixes.Contains(prefix), "Duplicate PIO prefix in RA");
+            SuccessOrQuit(pioPrefixes.PushBack(prefix));
 
             SuccessOrQuit(otBorderRoutingGetOnLinkPrefix(sInstance, &localOnLink));
-            VerifyOrQuit(prefix == localOnLink);
 
-            if (sExpectedPio == kPioAdvertisingLocalOnLink)
+            if (prefix == localOnLink)
             {
-                VerifyOrQuit(pio.GetPreferredLifetime() > 0, "On link prefix is deprecated unexpectedly");
+                switch (sExpectedPio)
+                {
+                case kNoPio:
+                    break;
+
+                case kPioAdvertisingLocalOnLink:
+                    if (pio.GetPreferredLifetime() > 0)
+                    {
+                        sOnLinkLifetime = pio.GetValidLifetime();
+                        sawExpectedPio  = true;
+                    }
+                    break;
+
+                case kPioDeprecatingLocalOnLink:
+                    if (pio.GetPreferredLifetime() == 0)
+                    {
+                        sOnLinkLifetime = pio.GetValidLifetime();
+                        sawExpectedPio  = true;
+                    }
+                    break;
+                }
             }
             else
             {
-                VerifyOrQuit(pio.GetPreferredLifetime() == 0, "On link prefix is not deprecated");
+                VerifyOrQuit(pio.GetPreferredLifetime() == 0, "Old on link prefix is not deprecated");
+                SuccessOrQuit(sDeprecatingPrefixes.PushBack(DeprecatingPrefix(prefix, pio.GetValidLifetime())));
             }
-
             break;
         }
 
@@ -254,9 +379,16 @@
             VerifyOrQuit(rio.IsValid());
             rio.GetPrefix(prefix);
 
-            if (prefix == sExpectedRioPrefix)
+            VerifyOrQuit(!rioPrefixes.Contains(prefix), "Duplicate RIO prefix in RA");
+            SuccessOrQuit(rioPrefixes.PushBack(prefix));
+
+            for (RioPrefix &rioPrefix : sExpectedRios)
             {
-                sSawExpectedRio = true;
+                if (prefix == rioPrefix.mPrefix)
+                {
+                    rioPrefix.mSawInRa  = true;
+                    rioPrefix.mLifetime = rio.GetRouteLifetime();
+                }
             }
 
             break;
@@ -266,6 +398,27 @@
             VerifyOrQuit(false, "Unexpected option type in RA msg");
         }
     }
+
+    if (!sRaValidated)
+    {
+        switch (sExpectedPio)
+        {
+        case kNoPio:
+            break;
+        case kPioAdvertisingLocalOnLink:
+        case kPioDeprecatingLocalOnLink:
+            // First emitted RAs may not yet have the expected PIO
+            // so we exit and not set `sRaValidated` to allow it
+            // to be checked for next received RA.
+            VerifyOrExit(sawExpectedPio);
+            break;
+        }
+
+        sRaValidated = true;
+    }
+
+exit:
+    return;
 }
 
 void LogRouterAdvert(const Icmp6Packet &aPacket)
@@ -312,7 +465,7 @@
     }
 }
 
-const char *PreferenceToString(uint8_t aPreference)
+const char *PreferenceToString(int8_t aPreference)
 {
     const char *str = "";
 
@@ -342,6 +495,13 @@
     otPlatInfraIfRecvIcmp6Nd(sInstance, kInfraIfIndex, &aAddress, aPacket.GetBytes(), aPacket.GetLength());
 }
 
+void SendNeighborAdvert(const Ip6::Address &aAddress, const Ip6::Nd::NeighborAdvertMessage &aNaMessage)
+{
+    Log("Sending NA from %s", aAddress.ToString().AsCString());
+    otPlatInfraIfRecvIcmp6Nd(sInstance, kInfraIfIndex, &aAddress, reinterpret_cast<const uint8_t *>(&aNaMessage),
+                             sizeof(aNaMessage));
+}
+
 Ip6::Prefix PrefixFromString(const char *aString, uint8_t aPrefixLength)
 {
     Ip6::Prefix prefix;
@@ -361,30 +521,308 @@
     return address;
 }
 
-void TestRoutingManager(void)
+void VerifyOmrPrefixInNetData(const Ip6::Prefix &aOmrPrefix, bool aDefaultRoute)
 {
-    Instance &                                        instance = *static_cast<Instance *>(testInitInstance());
-    BorderRouter::RoutingManager &                    rm       = instance.Get<BorderRouter::RoutingManager>();
-    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::Address                                      routerAddressA = AddressFromString("fd00::aaaa");
-    Ip6::Address                                      routerAddressB = AddressFromString("fd00::bbbb");
+    otNetworkDataIterator           iterator = OT_NETWORK_DATA_ITERATOR_INIT;
+    NetworkData::OnMeshPrefixConfig prefixConfig;
+
+    Log("VerifyOmrPrefixInNetData(%s, def-route:%s)", aOmrPrefix.ToString().AsCString(), aDefaultRoute ? "yes" : "no");
+
+    SuccessOrQuit(otNetDataGetNextOnMeshPrefix(sInstance, &iterator, &prefixConfig));
+    VerifyOrQuit(prefixConfig.GetPrefix() == aOmrPrefix);
+    VerifyOrQuit(prefixConfig.mStable == true);
+    VerifyOrQuit(prefixConfig.mSlaac == true);
+    VerifyOrQuit(prefixConfig.mPreferred == true);
+    VerifyOrQuit(prefixConfig.mOnMesh == true);
+    VerifyOrQuit(prefixConfig.mDefaultRoute == aDefaultRoute);
+
+    VerifyOrQuit(otNetDataGetNextOnMeshPrefix(sInstance, &iterator, &prefixConfig) == kErrorNotFound);
+}
+
+void VerifyNoOmrPrefixInNetData(void)
+{
+    otNetworkDataIterator           iterator = OT_NETWORK_DATA_ITERATOR_INIT;
+    NetworkData::OnMeshPrefixConfig prefixConfig;
+
+    Log("VerifyNoOmrPrefixInNetData()");
+    VerifyOrQuit(otNetDataGetNextOnMeshPrefix(sInstance, &iterator, &prefixConfig) != kErrorNone);
+}
+
+using NetworkData::RoutePreference;
+
+enum ExternalRouteMode : uint8_t
+{
+    kNoRoute,
+    kDefaultRoute,
+    kUlaRoute,
+};
+
+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;
+    }
+}
+
+struct Pio
+{
+    Pio(const Ip6::Prefix &aPrefix, uint32_t aValidLifetime, uint32_t aPreferredLifetime)
+        : mPrefix(aPrefix)
+        , mValidLifetime(aValidLifetime)
+        , mPreferredLifetime(aPreferredLifetime)
+    {
+    }
+
+    const Ip6::Prefix &mPrefix;
+    uint32_t           mValidLifetime;
+    uint32_t           mPreferredLifetime;
+};
+
+struct Rio
+{
+    Rio(const Ip6::Prefix &aPrefix, uint32_t aValidLifetime, RoutePreference aPreference)
+        : mPrefix(aPrefix)
+        , mValidLifetime(aValidLifetime)
+        , mPreference(aPreference)
+    {
+    }
+
+    const Ip6::Prefix &mPrefix;
+    uint32_t           mValidLifetime;
+    RoutePreference    mPreference;
+};
+
+struct DefaultRoute
+{
+    DefaultRoute(uint32_t aLifetime, RoutePreference aPreference)
+        : mLifetime(aLifetime)
+        , mPreference(aPreference)
+    {
+    }
+
+    uint32_t        mLifetime;
+    RoutePreference mPreference;
+};
+
+void SendRouterAdvert(const Ip6::Address &aRouterAddress,
+                      const Pio          *aPios,
+                      uint16_t            aNumPios,
+                      const Rio          *aRios,
+                      uint16_t            aNumRios,
+                      const DefaultRoute &aDefaultRoute)
+{
+    Ip6::Nd::RouterAdvertMessage::Header header;
+    uint8_t                              buffer[kMaxRaSize];
+
+    header.SetRouterLifetime(aDefaultRoute.mLifetime);
+    header.SetDefaultRouterPreference(aDefaultRoute.mPreference);
+
+    {
+        Ip6::Nd::RouterAdvertMessage raMsg(header, buffer);
+
+        for (; aNumPios > 0; aPios++, aNumPios--)
+        {
+            SuccessOrQuit(
+                raMsg.AppendPrefixInfoOption(aPios->mPrefix, aPios->mValidLifetime, aPios->mPreferredLifetime));
+        }
+
+        for (; aNumRios > 0; aRios++, aNumRios--)
+        {
+            SuccessOrQuit(raMsg.AppendRouteInfoOption(aRios->mPrefix, aRios->mValidLifetime, aRios->mPreference));
+        }
+
+        SendRouterAdvert(aRouterAddress, raMsg.GetAsPacket());
+        Log("Sending RA from router %s", aRouterAddress.ToString().AsCString());
+        LogRouterAdvert(raMsg.GetAsPacket());
+    }
+}
+
+template <uint16_t kNumPios, uint16_t kNumRios>
+void SendRouterAdvert(const Ip6::Address &aRouterAddress,
+                      const Pio (&aPios)[kNumPios],
+                      const Rio (&aRios)[kNumRios],
+                      const DefaultRoute &aDefaultRoute = DefaultRoute(0, NetworkData::kRoutePreferenceMedium))
+{
+    SendRouterAdvert(aRouterAddress, aPios, kNumPios, aRios, kNumRios, aDefaultRoute);
+}
+
+template <uint16_t kNumPios>
+void SendRouterAdvert(const Ip6::Address &aRouterAddress,
+                      const Pio (&aPios)[kNumPios],
+                      const DefaultRoute &aDefaultRoute = DefaultRoute(0, NetworkData::kRoutePreferenceMedium))
+{
+    SendRouterAdvert(aRouterAddress, aPios, kNumPios, nullptr, 0, aDefaultRoute);
+}
+
+template <uint16_t kNumRios>
+void SendRouterAdvert(const Ip6::Address &aRouterAddress,
+                      const Rio (&aRios)[kNumRios],
+                      const DefaultRoute &aDefaultRoute = DefaultRoute(0, NetworkData::kRoutePreferenceMedium))
+{
+    SendRouterAdvert(aRouterAddress, nullptr, 0, aRios, kNumRios, aDefaultRoute);
+}
+
+void SendRouterAdvert(const Ip6::Address &aRouterAddress, const DefaultRoute &aDefaultRoute)
+{
+    SendRouterAdvert(aRouterAddress, nullptr, 0, nullptr, 0, aDefaultRoute);
+}
+
+struct OnLinkPrefix : public Pio
+{
+    OnLinkPrefix(const Ip6::Prefix  &aPrefix,
+                 uint32_t            aValidLifetime,
+                 uint32_t            aPreferredLifetime,
+                 const Ip6::Address &aRouterAddress)
+        : Pio(aPrefix, aValidLifetime, aPreferredLifetime)
+        , mRouterAddress(aRouterAddress)
+    {
+    }
+
+    const Ip6::Address &mRouterAddress;
+};
+
+struct RoutePrefix : public Rio
+{
+    RoutePrefix(const Ip6::Prefix  &aPrefix,
+                uint32_t            aValidLifetime,
+                RoutePreference     aPreference,
+                const Ip6::Address &aRouterAddress)
+        : Rio(aPrefix, aValidLifetime, aPreference)
+        , mRouterAddress(aRouterAddress)
+    {
+    }
+
+    const Ip6::Address &mRouterAddress;
+};
+
+template <uint16_t kNumOnLinkPrefixes, uint16_t kNumRoutePrefixes>
+void VerifyPrefixTable(const OnLinkPrefix (&aOnLinkPrefixes)[kNumOnLinkPrefixes],
+                       const RoutePrefix (&aRoutePrefixes)[kNumRoutePrefixes])
+{
+    VerifyPrefixTable(aOnLinkPrefixes, kNumOnLinkPrefixes, aRoutePrefixes, kNumRoutePrefixes);
+}
+
+template <uint16_t kNumOnLinkPrefixes> void VerifyPrefixTable(const OnLinkPrefix (&aOnLinkPrefixes)[kNumOnLinkPrefixes])
+{
+    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,
+                       uint16_t            aNumRoutePrefixes)
+{
     BorderRouter::RoutingManager::PrefixTableIterator iter;
     BorderRouter::RoutingManager::PrefixTableEntry    entry;
-    NetworkData::Iterator                             iterator;
-    NetworkData::OnMeshPrefixConfig                   prefixConfig;
-    NetworkData::ExternalRouteConfig                  routeConfig;
-    uint8_t                                           counter;
-    uint8_t                                           buffer[800];
+    uint16_t                                          onLinkPrefixCount = 0;
+    uint16_t                                          routePrefixCount  = 0;
 
+    Log("VerifyPrefixTable()");
+
+    sInstance->Get<BorderRouter::RoutingManager>().InitPrefixTableIterator(iter);
+
+    while (sInstance->Get<BorderRouter::RoutingManager>().GetNextPrefixTableEntry(iter, entry) == kErrorNone)
+    {
+        bool didFind = false;
+
+        if (entry.mIsOnLink)
+        {
+            Log("   on-link prefix:%s, valid:%u, preferred:%u, router:%s, age:%u",
+                AsCoreType(&entry.mPrefix).ToString().AsCString(), entry.mValidLifetime, entry.mPreferredLifetime,
+                AsCoreType(&entry.mRouterAddress).ToString().AsCString(), entry.mMsecSinceLastUpdate / 1000);
+
+            onLinkPrefixCount++;
+
+            for (uint16_t index = 0; index < aNumOnLinkPrefixes; index++)
+            {
+                const OnLinkPrefix &onLinkPrefix = aOnLinkPrefixes[index];
+
+                if ((onLinkPrefix.mPrefix == AsCoreType(&entry.mPrefix)) &&
+                    (AsCoreType(&entry.mRouterAddress) == onLinkPrefix.mRouterAddress))
+                {
+                    VerifyOrQuit(entry.mValidLifetime == onLinkPrefix.mValidLifetime);
+                    VerifyOrQuit(entry.mPreferredLifetime == onLinkPrefix.mPreferredLifetime);
+                    didFind = true;
+                    break;
+                }
+            }
+        }
+        else
+        {
+            Log("   route prefix:%s, valid:%u, prf:%s, router:%s, age:%u",
+                AsCoreType(&entry.mPrefix).ToString().AsCString(), entry.mValidLifetime,
+                PreferenceToString(entry.mRoutePreference), AsCoreType(&entry.mRouterAddress).ToString().AsCString(),
+                entry.mMsecSinceLastUpdate / 1000);
+
+            routePrefixCount++;
+
+            for (uint16_t index = 0; index < aNumRoutePrefixes; index++)
+            {
+                const RoutePrefix &routePrefix = aRoutePrefixes[index];
+
+                if ((routePrefix.mPrefix == AsCoreType(&entry.mPrefix)) &&
+                    (AsCoreType(&entry.mRouterAddress) == routePrefix.mRouterAddress))
+                {
+                    VerifyOrQuit(entry.mValidLifetime == routePrefix.mValidLifetime);
+                    VerifyOrQuit(static_cast<int8_t>(entry.mRoutePreference) == routePrefix.mPreference);
+                    didFind = true;
+                    break;
+                }
+            }
+        }
+
+        VerifyOrQuit(didFind);
+    }
+
+    VerifyOrQuit(onLinkPrefixCount == aNumOnLinkPrefixes);
+    VerifyOrQuit(routePrefixCount == aNumRoutePrefixes);
+}
+
+void VerifyPrefixTableIsEmpty(void) { VerifyPrefixTable(nullptr, 0, nullptr, 0); }
+
+void InitTest(bool aEnablBorderRouting = false, bool aAfterReset = false)
+{
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Initialize OT instance.
 
     sNow      = 0;
-    sInstance = &instance;
+    sInstance = static_cast<Instance *>(testInitInstance());
+
+    uint32_t delay = 10000;
+    if (aAfterReset)
+    {
+        delay += 26000; // leader reset sync delay
+    }
 
     memset(&sRadioTxFrame, 0, sizeof(sRadioTxFrame));
     sRadioTxFrame.mPsdu = sRadioTxFramePsdu;
@@ -392,218 +830,131 @@
     SuccessOrQuit(sInfraIfAddress.FromString(kInfraIfAddress));
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Initialize Border Router and start Thread operation.
+    // Initialize and start Border Router and Thread operation.
 
     SuccessOrQuit(otBorderRoutingInit(sInstance, kInfraIfIndex, /* aInfraIfIsRunning */ true));
 
     SuccessOrQuit(otLinkSetPanId(sInstance, 0x1234));
     SuccessOrQuit(otIp6SetEnabled(sInstance, true));
     SuccessOrQuit(otThreadSetEnabled(sInstance, true));
+    SuccessOrQuit(otBorderRoutingSetEnabled(sInstance, aEnablBorderRouting));
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Ensure device starts as leader.
 
-    AdvanceTime(10000);
+    AdvanceTime(delay);
 
     VerifyOrQuit(otThreadGetDeviceRole(sInstance) == OT_DEVICE_ROLE_LEADER);
 
+    // Reset all test flags
+    sRsEmitted   = false;
+    sRaValidated = false;
+    sExpectedPio = kNoPio;
+    sExpectedRios.Clear();
+    sRespondToNs = true;
+}
+
+void FinalizeTest(void)
+{
+    SuccessOrQuit(otIp6SetEnabled(sInstance, false));
+    SuccessOrQuit(otThreadSetEnabled(sInstance, false));
+    SuccessOrQuit(otInstanceErasePersistentInfo(sInstance));
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void TestSamePrefixesFromMultipleRouters(void)
+{
+    Ip6::Prefix  localOnLink;
+    Ip6::Prefix  localOmr;
+    Ip6::Prefix  onLinkPrefix   = PrefixFromString("2000:abba:baba::", 64);
+    Ip6::Prefix  routePrefix    = PrefixFromString("2000:1234:5678::", 64);
+    Ip6::Address routerAddressA = AddressFromString("fd00::aaaa");
+    Ip6::Address routerAddressB = AddressFromString("fd00::bbbb");
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestSamePrefixesFromMultipleRouters");
+
+    InitTest();
+
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Start Routing Manager. Check emitted RS and RA messages.
 
-    sRsEmitted      = false;
-    sRaValidated    = false;
-    sSawExpectedRio = false;
-    sExpectedPio    = kPioAdvertisingLocalOnLink;
+    sRsEmitted   = false;
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Clear();
 
-    Log("Starting RoutingManager");
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(true));
 
-    SuccessOrQuit(rm.SetEnabled(true));
-
-    SuccessOrQuit(rm.GetOnLinkPrefix(localOnLink));
-    SuccessOrQuit(rm.GetOmrPrefix(localOmr));
+    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());
 
-    sExpectedRioPrefix = localOmr;
+    sExpectedRios.Add(localOmr);
 
     AdvanceTime(30000);
 
     VerifyOrQuit(sRsEmitted);
     VerifyOrQuit(sRaValidated);
-    VerifyOrQuit(sSawExpectedRio);
+    VerifyOrQuit(sExpectedRios.SawAll());
     Log("Received RA was validated");
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data to include the local OMR and on-link prefix.
 
-    iterator = NetworkData::kIteratorInit;
-
-    // We expect to see OMR prefix in net data as on-mesh prefix.
-    SuccessOrQuit(instance.Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig));
-    VerifyOrQuit(prefixConfig.GetPrefix() == localOmr);
-    VerifyOrQuit(prefixConfig.mStable == true);
-    VerifyOrQuit(prefixConfig.mSlaac == true);
-    VerifyOrQuit(prefixConfig.mPreferred == true);
-    VerifyOrQuit(prefixConfig.mOnMesh == true);
-    VerifyOrQuit(prefixConfig.mDefaultRoute == false);
-
-    VerifyOrQuit(instance.Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig) == kErrorNotFound);
-
-    iterator = NetworkData::kIteratorInit;
-
-    // We expect to see local on-link prefix in net data as external route.
-    SuccessOrQuit(instance.Get<NetworkData::Leader>().GetNextExternalRoute(iterator, routeConfig));
-    VerifyOrQuit(routeConfig.GetPrefix() == localOnLink);
-    VerifyOrQuit(routeConfig.mStable == true);
-    VerifyOrQuit(routeConfig.mPreference == NetworkData::kRoutePreferenceMedium);
-
-    VerifyOrQuit(instance.Get<NetworkData::Leader>().GetNextExternalRoute(iterator, routeConfig) == kErrorNotFound);
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router A with a new on-link (PIO) and route prefix (RIO).
 
-    {
-        Ip6::Nd::RouterAdvertMessage raMsg(Ip6::Nd::RouterAdvertMessage::Header(), buffer);
-
-        SuccessOrQuit(raMsg.AppendPrefixInfoOption(onLinkPrefix, kValidLitime, kPreferredLifetime));
-        SuccessOrQuit(raMsg.AppendRouteInfoOption(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium));
-
-        SendRouterAdvert(routerAddressA, raMsg.GetAsPacket());
-
-        Log("Send RA from router A");
-        LogRouterAdvert(raMsg.GetAsPacket());
-    }
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)},
+                     {Rio(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check that the local on-link prefix is now deprecating in the new RA.
 
-    sRaValidated    = false;
-    sSawExpectedRio = false;
-    sExpectedPio    = kPioDeprecatingLocalOnLink;
+    sRaValidated = false;
+    sExpectedPio = kPioDeprecatingLocalOnLink;
 
     AdvanceTime(10000);
     VerifyOrQuit(sRaValidated);
-    VerifyOrQuit(sSawExpectedRio);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check the discovered prefix table and ensure info from router A
     // is present in the table.
 
-    counter = 0;
-
-    rm.InitPrefixTableIterator(iter);
-
-    while (rm.GetNextPrefixTableEntry(iter, entry) == kErrorNone)
-    {
-        counter++;
-        VerifyOrQuit(AsCoreType(&entry.mRouterAddress) == routerAddressA);
-
-        if (entry.mIsOnLink)
-        {
-            VerifyOrQuit(AsCoreType(&entry.mPrefix) == onLinkPrefix);
-            VerifyOrQuit(entry.mValidLifetime = kValidLitime);
-            VerifyOrQuit(entry.mPreferredLifetime = kPreferredLifetime);
-        }
-        else
-        {
-            VerifyOrQuit(AsCoreType(&entry.mPrefix) == routePrefix);
-            VerifyOrQuit(entry.mValidLifetime = kValidLitime);
-            VerifyOrQuit(static_cast<int8_t>(entry.mRoutePreference) == NetworkData::kRoutePreferenceMedium);
-        }
-    }
-
-    VerifyOrQuit(counter == 2);
+    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, kPreferredLifetime, routerAddressA)},
+                      {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data to include new prefixes from router A.
 
-    iterator = NetworkData::kIteratorInit;
-
-    // We expect to see OMR prefix in net data as on-mesh prefix.
-    SuccessOrQuit(instance.Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig));
-    VerifyOrQuit(prefixConfig.GetPrefix() == localOmr);
-    VerifyOrQuit(prefixConfig.mStable == true);
-    VerifyOrQuit(prefixConfig.mSlaac == true);
-    VerifyOrQuit(prefixConfig.mPreferred == true);
-    VerifyOrQuit(prefixConfig.mOnMesh == true);
-    VerifyOrQuit(prefixConfig.mDefaultRoute == false);
-
-    VerifyOrQuit(instance.Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig) == kErrorNotFound);
-
-    iterator = NetworkData::kIteratorInit;
-
-    counter = 0;
-
-    // We expect to see 3 entries, our local on link and new prefixes from router A.
-    while (instance.Get<NetworkData::Leader>().GetNextExternalRoute(iterator, routeConfig) == kErrorNone)
-    {
-        VerifyOrQuit((routeConfig.GetPrefix() == localOnLink) || (routeConfig.GetPrefix() == onLinkPrefix) ||
-                     (routeConfig.GetPrefix() == routePrefix));
-        VerifyOrQuit(static_cast<int8_t>(routeConfig.mPreference) == NetworkData::kRoutePreferenceMedium);
-        counter++;
-    }
-
-    VerifyOrQuit(counter == 3);
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send the same RA again from router A with the on-link (PIO) and route prefix (RIO).
 
-    {
-        Ip6::Nd::RouterAdvertMessage raMsg(Ip6::Nd::RouterAdvertMessage::Header(), buffer);
-
-        SuccessOrQuit(raMsg.AppendPrefixInfoOption(onLinkPrefix, kValidLitime, kPreferredLifetime));
-        SuccessOrQuit(raMsg.AppendRouteInfoOption(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium));
-
-        SendRouterAdvert(routerAddressA, raMsg.GetAsPacket());
-
-        Log("Send RA from router A");
-        LogRouterAdvert(raMsg.GetAsPacket());
-    }
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)},
+                     {Rio(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check the discovered prefix table and ensure info from router A
     // remains unchanged.
 
-    counter = 0;
-
-    rm.InitPrefixTableIterator(iter);
-
-    while (rm.GetNextPrefixTableEntry(iter, entry) == kErrorNone)
-    {
-        counter++;
-        VerifyOrQuit(AsCoreType(&entry.mRouterAddress) == routerAddressA);
-
-        if (entry.mIsOnLink)
-        {
-            VerifyOrQuit(AsCoreType(&entry.mPrefix) == onLinkPrefix);
-            VerifyOrQuit(entry.mValidLifetime = kValidLitime);
-            VerifyOrQuit(entry.mPreferredLifetime = kPreferredLifetime);
-        }
-        else
-        {
-            VerifyOrQuit(AsCoreType(&entry.mPrefix) == routePrefix);
-            VerifyOrQuit(entry.mValidLifetime = kValidLitime);
-            VerifyOrQuit(static_cast<int8_t>(entry.mRoutePreference) == NetworkData::kRoutePreferenceMedium);
-        }
-    }
-
-    VerifyOrQuit(counter == 2);
+    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, kPreferredLifetime, routerAddressA)},
+                      {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router B with same route prefix (RIO) but with
     // high route preference.
 
-    {
-        Ip6::Nd::RouterAdvertMessage raMsg(Ip6::Nd::RouterAdvertMessage::Header(), buffer);
-
-        SuccessOrQuit(raMsg.AppendRouteInfoOption(routePrefix, kValidLitime, NetworkData::kRoutePreferenceHigh));
-
-        SendRouterAdvert(routerAddressB, raMsg.GetAsPacket());
-
-        Log("Send RA from router B");
-        LogRouterAdvert(raMsg.GetAsPacket());
-    }
+    SendRouterAdvert(routerAddressB, {Rio(routePrefix, kValidLitime, NetworkData::kRoutePreferenceHigh)});
 
     AdvanceTime(10000);
 
@@ -611,99 +962,20 @@
     // Check the discovered prefix table and ensure info from router B
     // is also included in the table.
 
-    counter = 0;
-
-    rm.InitPrefixTableIterator(iter);
-
-    while (rm.GetNextPrefixTableEntry(iter, entry) == kErrorNone)
-    {
-        const Ip6::Address &routerAddr = AsCoreType(&entry.mRouterAddress);
-        counter++;
-
-        if (routerAddr == routerAddressA)
-        {
-            if (entry.mIsOnLink)
-            {
-                VerifyOrQuit(AsCoreType(&entry.mPrefix) == onLinkPrefix);
-                VerifyOrQuit(entry.mValidLifetime = kValidLitime);
-                VerifyOrQuit(entry.mPreferredLifetime = kPreferredLifetime);
-            }
-            else
-            {
-                VerifyOrQuit(AsCoreType(&entry.mPrefix) == routePrefix);
-                VerifyOrQuit(entry.mValidLifetime = kValidLitime);
-                VerifyOrQuit(static_cast<int8_t>(entry.mRoutePreference) == NetworkData::kRoutePreferenceMedium);
-            }
-        }
-        else if (routerAddr == routerAddressB)
-        {
-            VerifyOrQuit(!entry.mIsOnLink);
-            VerifyOrQuit(AsCoreType(&entry.mPrefix) == routePrefix);
-            VerifyOrQuit(entry.mValidLifetime = kValidLitime);
-            VerifyOrQuit(static_cast<int8_t>(entry.mRoutePreference) == NetworkData::kRoutePreferenceHigh);
-        }
-        else
-        {
-            VerifyOrQuit(false, "Unexpected entry in prefix table with unknown router address");
-        }
-    }
-
-    VerifyOrQuit(counter == 3);
+    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, kPreferredLifetime, routerAddressA)},
+                      {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA),
+                       RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceHigh, routerAddressB)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data.
 
-    iterator = NetworkData::kIteratorInit;
-
-    // We expect to see OMR prefix in net data as on-mesh prefix
-    SuccessOrQuit(instance.Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig));
-    VerifyOrQuit(prefixConfig.GetPrefix() == localOmr);
-    VerifyOrQuit(prefixConfig.mStable == true);
-    VerifyOrQuit(prefixConfig.mSlaac == true);
-    VerifyOrQuit(prefixConfig.mPreferred == true);
-    VerifyOrQuit(prefixConfig.mOnMesh == true);
-    VerifyOrQuit(prefixConfig.mDefaultRoute == false);
-
-    VerifyOrQuit(instance.Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig) == kErrorNotFound);
-
-    iterator = NetworkData::kIteratorInit;
-
-    counter = 0;
-
-    // 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.
-
-    while (instance.Get<NetworkData::Leader>().GetNextExternalRoute(iterator, routeConfig) == kErrorNone)
-    {
-        counter++;
-
-        if (routeConfig.GetPrefix() == routePrefix)
-        {
-            VerifyOrQuit(static_cast<int8_t>(routeConfig.mPreference) == NetworkData::kRoutePreferenceHigh);
-        }
-        else
-        {
-            VerifyOrQuit((routeConfig.GetPrefix() == localOnLink) || (routeConfig.GetPrefix() == onLinkPrefix));
-            VerifyOrQuit(static_cast<int8_t>(routeConfig.mPreference) == NetworkData::kRoutePreferenceMedium);
-        }
-    }
-
-    VerifyOrQuit(counter == 3);
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router B removing the route prefix.
 
-    {
-        Ip6::Nd::RouterAdvertMessage raMsg(Ip6::Nd::RouterAdvertMessage::Header(), buffer);
-
-        SuccessOrQuit(raMsg.AppendRouteInfoOption(routePrefix, 0, NetworkData::kRoutePreferenceHigh));
-
-        SendRouterAdvert(routerAddressB, raMsg.GetAsPacket());
-
-        Log("Send RA from router B");
-        LogRouterAdvert(raMsg.GetAsPacket());
-    }
+    SendRouterAdvert(routerAddressB, {Rio(routePrefix, 0, NetworkData::kRoutePreferenceHigh)});
 
     AdvanceTime(10000);
 
@@ -711,48 +983,63 @@
     // Check the discovered prefix table and ensure info from router B
     // is now removed from the table.
 
-    counter = 0;
-
-    rm.InitPrefixTableIterator(iter);
-
-    while (rm.GetNextPrefixTableEntry(iter, entry) == kErrorNone)
-    {
-        counter++;
-
-        VerifyOrQuit(AsCoreType(&entry.mRouterAddress) == routerAddressA);
-
-        if (entry.mIsOnLink)
-        {
-            VerifyOrQuit(AsCoreType(&entry.mPrefix) == onLinkPrefix);
-            VerifyOrQuit(entry.mValidLifetime = kValidLitime);
-            VerifyOrQuit(entry.mPreferredLifetime = kPreferredLifetime);
-        }
-        else
-        {
-            VerifyOrQuit(AsCoreType(&entry.mPrefix) == routePrefix);
-            VerifyOrQuit(entry.mValidLifetime = kValidLitime);
-            VerifyOrQuit(static_cast<int8_t>(entry.mRoutePreference) == NetworkData::kRoutePreferenceMedium);
-        }
-    }
-
-    VerifyOrQuit(counter == 2);
+    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, kPreferredLifetime, routerAddressA)},
+                      {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data, all prefixes should be again at medium preference.
+    // Check Network Data.
 
-    counter  = 0;
-    iterator = NetworkData::kIteratorInit;
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
-    while (instance.Get<NetworkData::Leader>().GetNextExternalRoute(iterator, routeConfig) == kErrorNone)
-    {
-        counter++;
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
-        VerifyOrQuit((routeConfig.GetPrefix() == localOnLink) || (routeConfig.GetPrefix() == onLinkPrefix) ||
-                     (routeConfig.GetPrefix() == routePrefix));
-        VerifyOrQuit(static_cast<int8_t>(routeConfig.mPreference) == NetworkData::kRoutePreferenceMedium);
-    }
+    Log("End of TestSamePrefixesFromMultipleRouters");
 
-    VerifyOrQuit(counter == 3);
+    FinalizeTest();
+}
+
+void TestOmrSelection(void)
+{
+    Ip6::Prefix                     localOnLink;
+    Ip6::Prefix                     localOmr;
+    Ip6::Prefix                     omrPrefix = PrefixFromString("2000:0000:1111:4444::", 64);
+    NetworkData::OnMeshPrefixConfig prefixConfig;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestOmrSelection");
+
+    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 on-link prefix.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Add a new OMR prefix directly into net data. The new prefix should
@@ -775,45 +1062,22 @@
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Make sure BR emits RA with new OMR prefix now.
 
-    sRaValidated       = false;
-    sSawExpectedRio    = false;
-    sExpectedRioPrefix = omrPrefix;
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Clear();
+    sExpectedRios.Add(omrPrefix);
 
     AdvanceTime(20000);
 
     VerifyOrQuit(sRaValidated);
-    VerifyOrQuit(sSawExpectedRio);
+    VerifyOrQuit(sExpectedRios.SawAll());
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data. We should now see that the local OMR prefix
     // is removed.
 
-    iterator = NetworkData::kIteratorInit;
-
-    // We expect to see new OMR prefix in net data.
-    SuccessOrQuit(instance.Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig));
-    VerifyOrQuit(prefixConfig.GetPrefix() == omrPrefix);
-    VerifyOrQuit(prefixConfig.mStable == true);
-    VerifyOrQuit(prefixConfig.mSlaac == true);
-    VerifyOrQuit(prefixConfig.mPreferred == true);
-    VerifyOrQuit(prefixConfig.mOnMesh == true);
-    VerifyOrQuit(prefixConfig.mDefaultRoute == false);
-
-    VerifyOrQuit(instance.Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig) == kErrorNotFound);
-
-    counter  = 0;
-    iterator = NetworkData::kIteratorInit;
-
-    while (instance.Get<NetworkData::Leader>().GetNextExternalRoute(iterator, routeConfig) == kErrorNone)
-    {
-        counter++;
-
-        VerifyOrQuit((routeConfig.GetPrefix() == localOnLink) || (routeConfig.GetPrefix() == onLinkPrefix) ||
-                     (routeConfig.GetPrefix() == routePrefix));
-        VerifyOrQuit(static_cast<int8_t>(routeConfig.mPreference) == NetworkData::kRoutePreferenceMedium);
-    }
-
-    VerifyOrQuit(counter == 3);
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Remove the OMR prefix previously added in net data.
@@ -826,59 +1090,1942 @@
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Make sure BR emits RA with local OMR prefix again.
 
-    sRaValidated       = false;
-    sSawExpectedRio    = false;
-    sExpectedRioPrefix = localOmr;
+    sRaValidated = false;
+    sExpectedRios.Clear();
+    sExpectedRios.Add(localOmr);
 
     AdvanceTime(20000);
 
     VerifyOrQuit(sRaValidated);
-    VerifyOrQuit(sSawExpectedRio);
+    VerifyOrQuit(sExpectedRios.SawAll());
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data. We should see that the local OMR prefix is
     // added again.
 
-    iterator = NetworkData::kIteratorInit;
-
-    // We expect to see new OMR prefix in net data as on-mesh prefix.
-    SuccessOrQuit(instance.Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig));
-    VerifyOrQuit(prefixConfig.GetPrefix() == localOmr);
-    VerifyOrQuit(prefixConfig.mStable == true);
-    VerifyOrQuit(prefixConfig.mSlaac == true);
-    VerifyOrQuit(prefixConfig.mPreferred == true);
-    VerifyOrQuit(prefixConfig.mOnMesh == true);
-    VerifyOrQuit(prefixConfig.mDefaultRoute == false);
-
-    VerifyOrQuit(instance.Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig) == kErrorNotFound);
-
-    counter  = 0;
-    iterator = NetworkData::kIteratorInit;
-
-    while (instance.Get<NetworkData::Leader>().GetNextExternalRoute(iterator, routeConfig) == kErrorNone)
-    {
-        counter++;
-
-        VerifyOrQuit((routeConfig.GetPrefix() == localOnLink) || (routeConfig.GetPrefix() == onLinkPrefix) ||
-                     (routeConfig.GetPrefix() == routePrefix));
-        VerifyOrQuit(static_cast<int8_t>(routeConfig.mPreference) == NetworkData::kRoutePreferenceMedium);
-    }
-
-    VerifyOrQuit(counter == 3);
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
-    Log("End of test");
-
-    testFreeInstance(&instance);
+    Log("End of TestOmrSelection");
+    FinalizeTest();
 }
 
+void TestDefaultRoute(void)
+{
+    Ip6::Prefix                     localOnLink;
+    Ip6::Prefix                     localOmr;
+    Ip6::Prefix                     omrPrefix      = PrefixFromString("2000:0000:1111:4444::", 64);
+    Ip6::Prefix                     defaultRoute   = PrefixFromString("::", 0);
+    Ip6::Address                    routerAddressA = AddressFromString("fd00::aaaa");
+    NetworkData::OnMeshPrefixConfig prefixConfig;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestDefaultRoute");
+
+    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 default route.
+
+    SendRouterAdvert(routerAddressA, DefaultRoute(kValidLitime, NetworkData::kRoutePreferenceLow));
+
+    AdvanceTime(10000);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check the discovered prefix table and ensure default route
+    // from router A is in the table.
+
+    VerifyPrefixTable({RoutePrefix(defaultRoute, kValidLitime, NetworkData::kRoutePreferenceLow, 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;
+    prefixConfig.mStable       = true;
+    prefixConfig.mSlaac        = true;
+    prefixConfig.mPreferred    = true;
+    prefixConfig.mOnMesh       = true;
+    prefixConfig.mDefaultRoute = true;
+    prefixConfig.mPreference   = NetworkData::kRoutePreferenceMedium;
+
+    SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig));
+    SuccessOrQuit(otBorderRouterRegister(sInstance));
+
+    AdvanceTime(10000);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. Now that we have an infrastructure-derived
+    // OMR prefix, the default route should be published.
+
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Remove the OMR prefix from Network Data.
+
+    SuccessOrQuit(otBorderRouterRemoveOnMeshPrefix(sInstance, &omrPrefix));
+    SuccessOrQuit(otBorderRouterRegister(sInstance));
+
+    AdvanceTime(10000);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // 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.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    VerifyPrefixTable({RoutePrefix(defaultRoute, kValidLitime, NetworkData::kRoutePreferenceLow, routerAddressA)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // 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;
+    prefixConfig.mStable       = true;
+    prefixConfig.mSlaac        = true;
+    prefixConfig.mPreferred    = true;
+    prefixConfig.mOnMesh       = true;
+    prefixConfig.mDefaultRoute = true;
+    prefixConfig.mPreference   = NetworkData::kRoutePreferenceMedium;
+
+    SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig));
+    SuccessOrQuit(otBorderRouterRegister(sInstance));
+
+    AdvanceTime(10000);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. Now that we have an infrastructure-derived
+    // OMR prefix, the default route should be published.
+
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Remove the OMR prefix from Network Data.
+
+    SuccessOrQuit(otBorderRouterRemoveOnMeshPrefix(sInstance, &omrPrefix));
+    SuccessOrQuit(otBorderRouterRegister(sInstance));
+
+    AdvanceTime(10000);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // 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.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    VerifyPrefixTable({RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // 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 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 TestAdvNonUlaRoute");
+
+    FinalizeTest();
+}
+
+void TestLocalOnLinkPrefixDeprecation(void)
+{
+    static constexpr uint32_t kMaxRaTxInterval = 601; // In seconds
+
+    Ip6::Prefix  localOnLink;
+    Ip6::Prefix  localOmr;
+    Ip6::Prefix  onLinkPrefix   = PrefixFromString("fd00:abba:baba::", 64);
+    Ip6::Address routerAddressA = AddressFromString("fd00::aaaa");
+    uint32_t     localOnLinkLifetime;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestLocalOnLinkPrefixDeprecation");
+
+    InitTest();
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start Routing Manager. Check emitted RS and RA messages.
+
+    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());
+
+    sRsEmitted   = false;
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Clear();
+    sExpectedRios.Add(localOmr);
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRsEmitted);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+    Log("Local on-link prefix is being advertised, lifetime: %d", sOnLinkLifetime);
+    localOnLinkLifetime = sOnLinkLifetime;
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data to include the local OMR and on-link prefix.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router A with a new on-link (PIO) which is preferred over
+    // the local on-link prefix.
+
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that the local on-link prefix is now deprecating in the new RA.
+
+    sRaValidated = false;
+    sExpectedPio = kPioDeprecatingLocalOnLink;
+    sExpectedRios.Clear();
+    sExpectedRios.Add(localOmr);
+
+    AdvanceTime(10000);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+    Log("On-link prefix is deprecating, remaining lifetime:%d", sOnLinkLifetime);
+    VerifyOrQuit(sOnLinkLifetime < localOnLinkLifetime);
+    localOnLinkLifetime = sOnLinkLifetime;
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. We must see the new on-link prefix from router A
+    // along with the deprecating local on-link prefix.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Wait for local on-link prefix to expire
+
+    while (localOnLinkLifetime > kMaxRaTxInterval)
+    {
+        // Send same RA from router A to keep the on-link prefix alive.
+
+        SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
+        // Ensure Network Data entries remain as before. Mainly we still
+        // see the deprecating local on-link prefix.
+
+        VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+        VerifyExternalRouteInNetData(kUlaRoute);
+
+        // Keep checking the emitted RAs and make sure on-link prefix
+        // is included with smaller lifetime every time.
+
+        sRaValidated = false;
+        sExpectedPio = kPioDeprecatingLocalOnLink;
+        sExpectedRios.Clear();
+        sExpectedRios.Add(localOmr);
+
+        AdvanceTime(kMaxRaTxInterval * 1000);
+
+        VerifyOrQuit(sRaValidated);
+        VerifyOrQuit(sExpectedRios.SawAll());
+        Log("On-link prefix is deprecating, remaining lifetime:%d", sOnLinkLifetime);
+        VerifyOrQuit(sOnLinkLifetime < localOnLinkLifetime);
+        localOnLinkLifetime = sOnLinkLifetime;
+    }
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // The local on-link prefix must be expired and should no
+    // longer be seen in the emitted RA message.
+
+    sRaValidated = false;
+    sExpectedPio = kNoPio;
+    sExpectedRios.Clear();
+    sExpectedRios.Add(localOmr);
+
+    AdvanceTime(kMaxRaTxInterval * 1000);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+    Log("On-link prefix is now expired");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    Log("End of TestLocalOnLinkPrefixDeprecation");
+
+    FinalizeTest();
+}
+
+#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
+void TestDomainPrefixAsOmr(void)
+{
+    Ip6::Prefix                     localOnLink;
+    Ip6::Prefix                     localOmr;
+    Ip6::Prefix                     domainPrefix = PrefixFromString("2000:0000:1111:4444::", 64);
+    NetworkData::OnMeshPrefixConfig prefixConfig;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestDomainPrefixAsOmr");
+
+    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 on-link prefix.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Add a domain prefix directly into net data. The new prefix should
+    // be favored over the local OMR prefix.
+
+    otBackboneRouterSetEnabled(sInstance, true);
+
+    prefixConfig.Clear();
+    prefixConfig.mPrefix       = domainPrefix;
+    prefixConfig.mStable       = true;
+    prefixConfig.mSlaac        = true;
+    prefixConfig.mPreferred    = true;
+    prefixConfig.mOnMesh       = true;
+    prefixConfig.mDefaultRoute = false;
+    prefixConfig.mDp           = true;
+    prefixConfig.mPreference   = NetworkData::kRoutePreferenceMedium;
+
+    SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig));
+    SuccessOrQuit(otBorderRouterRegister(sInstance));
+
+    AdvanceTime(100);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Make sure BR emits RA without domain prefix or previous local OMR.
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Clear();
+    sExpectedRios.Add(domainPrefix);
+    sExpectedRios.Add(localOmr);
+
+    AdvanceTime(20000);
+
+    VerifyOrQuit(sRaValidated);
+
+    // We should see RIO removing the local OMR prefix with lifetime zero
+    // and should not see the domain prefix as RIO.
+
+    VerifyOrQuit(sExpectedRios[0].mPrefix == domainPrefix);
+    VerifyOrQuit(!sExpectedRios[0].mSawInRa);
+
+    VerifyOrQuit(sExpectedRios[1].mPrefix == localOmr);
+    VerifyOrQuit(sExpectedRios[1].mSawInRa);
+    VerifyOrQuit(sExpectedRios[1].mLifetime == 0);
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Clear();
+    sExpectedRios.Add(domainPrefix);
+    sExpectedRios.Add(localOmr);
+
+    // Wait for next RA (650 seconds).
+
+    AdvanceTime(650000);
+
+    VerifyOrQuit(sRaValidated);
+
+    // We should not see either domain prefix or local OMR
+    // as RIO.
+
+    VerifyOrQuit(!sExpectedRios[0].mSawInRa);
+    VerifyOrQuit(!sExpectedRios[1].mSawInRa);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. We should now see that the local OMR prefix
+    // is removed.
+
+    VerifyOmrPrefixInNetData(domainPrefix, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Remove the domain prefix from net data.
+
+    SuccessOrQuit(otBorderRouterRemoveOnMeshPrefix(sInstance, &domainPrefix));
+    SuccessOrQuit(otBorderRouterRegister(sInstance));
+
+    AdvanceTime(100);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Make sure BR emits RA with local OMR prefix again.
+
+    sRaValidated = false;
+    sExpectedRios.Clear();
+    sExpectedRios.Add(localOmr);
+
+    AdvanceTime(20000);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. We should see that the local OMR prefix is
+    // added again.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    Log("End of TestDomainPrefixAsOmr");
+    FinalizeTest();
+}
+#endif // OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
+
+void TestExtPanIdChange(void)
+{
+    static constexpr uint32_t kMaxRaTxInterval = 601; // In seconds
+
+    static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x6, 0x7, 0x08}};
+    static const otExtendedPanId kExtPanId2 = {{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x99, 0x88}};
+    static const otExtendedPanId kExtPanId3 = {{0x12, 0x34, 0x56, 0x78, 0x9a, 0xab, 0xcd, 0xef}};
+    static const otExtendedPanId kExtPanId4 = {{0x44, 0x00, 0x44, 0x00, 0x44, 0x00, 0x44, 0x00}};
+    static const otExtendedPanId kExtPanId5 = {{0x77, 0x88, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55}};
+
+    Ip6::Prefix          localOnLink;
+    Ip6::Prefix          oldLocalOnLink;
+    Ip6::Prefix          localOmr;
+    Ip6::Prefix          onLinkPrefix   = PrefixFromString("2000:abba:baba::", 64);
+    Ip6::Address         routerAddressA = AddressFromString("fd00::aaaa");
+    uint32_t             oldPrefixLifetime;
+    Ip6::Prefix          oldPrefixes[4];
+    otOperationalDataset dataset;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestExtPanIdChange");
+
+    InitTest();
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start Routing Manager. Check emitted RS and RA messages.
+
+    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());
+
+    sRsEmitted   = false;
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Clear();
+    sExpectedRios.Add(localOmr);
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRsEmitted);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+    Log("Local on-link prefix is being advertised, lifetime: %d", sOnLinkLifetime);
+
+    //= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
+    // Check behavior when ext PAN ID changes while the local on-link is
+    // being advertised.
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Change the extended PAN ID.
+
+    Log("Changing ext PAN ID");
+
+    oldLocalOnLink    = localOnLink;
+    oldPrefixLifetime = sOnLinkLifetime;
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    SuccessOrQuit(otDatasetGetActive(sInstance, &dataset));
+
+    VerifyOrQuit(dataset.mComponents.mIsExtendedPanIdPresent);
+
+    dataset.mExtendedPanId = kExtPanId1;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+
+    AdvanceTime(500);
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+    Log("Local on-link prefix changed to %s from %s", localOnLink.ToString().AsCString(),
+        oldLocalOnLink.ToString().AsCString());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate the received RA message and that it contains the
+    // old on-link prefix being deprecated.
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
+    VerifyOrQuit(sDeprecatingPrefixes[0].mPrefix == oldLocalOnLink);
+    oldPrefixLifetime = sDeprecatingPrefixes[0].mLifetime;
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate Network Data.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Stop BR and validate that a final RA is emitted deprecating
+    // both current local on-link prefix and old prefix.
+
+    sRaValidated = false;
+    sExpectedPio = kPioDeprecatingLocalOnLink;
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(false));
+    AdvanceTime(100);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
+    VerifyOrQuit(sDeprecatingPrefixes[0].mPrefix == oldLocalOnLink);
+    oldPrefixLifetime = sDeprecatingPrefixes[0].mLifetime;
+
+    sRaValidated = false;
+    AdvanceTime(350000);
+    VerifyOrQuit(!sRaValidated);
+
+    VerifyNoOmrPrefixInNetData();
+    VerifyExternalRouteInNetData(kNoRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start BR again and validate old prefix will continue to
+    // be deprecated.
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(true));
+
+    AdvanceTime(300000);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
+    VerifyOrQuit(sDeprecatingPrefixes[0].mPrefix == oldLocalOnLink);
+    VerifyOrQuit(oldPrefixLifetime > sDeprecatingPrefixes[0].mLifetime);
+    oldPrefixLifetime = sDeprecatingPrefixes[0].mLifetime;
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Wait for old local on-link prefix to expire.
+
+    while (oldPrefixLifetime > 2 * kMaxRaTxInterval)
+    {
+        // Ensure Network Data entries remain as before.
+
+        VerifyExternalRouteInNetData(kUlaRoute);
+
+        // Keep checking the emitted RAs and make sure the prefix
+        // is included with smaller lifetime every time.
+
+        sRaValidated = false;
+
+        AdvanceTime(kMaxRaTxInterval * 1000);
+
+        VerifyOrQuit(sRaValidated);
+        VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
+        Log("Old on-link prefix is deprecating, remaining lifetime:%d", sDeprecatingPrefixes[0].mLifetime);
+        VerifyOrQuit(sDeprecatingPrefixes[0].mLifetime < oldPrefixLifetime);
+        oldPrefixLifetime = sDeprecatingPrefixes[0].mLifetime;
+    }
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // The local on-link prefix must be expired now and should no
+    // longer be seen in the emitted RA message.
+
+    sRaValidated = false;
+
+    AdvanceTime(3 * kMaxRaTxInterval * 1000);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.IsEmpty());
+    Log("Old on-link prefix is now expired");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate the Network Data.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
+    // Check behavior when ext PAN ID changes while the local on-link is being
+    // deprecated.
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router A with a new on-link (PIO) which is preferred over
+    // the local on-link prefix.
+
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate that the local on-link prefix is deprecated.
+
+    sRaValidated = false;
+    sExpectedPio = kPioDeprecatingLocalOnLink;
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRaValidated);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Change the extended PAN ID.
+
+    oldLocalOnLink    = localOnLink;
+    oldPrefixLifetime = sOnLinkLifetime;
+
+    dataset.mExtendedPanId = kExtPanId2;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+
+    AdvanceTime(500);
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+    Log("Local on-link prefix changed to %s from %s", localOnLink.ToString().AsCString(),
+        oldLocalOnLink.ToString().AsCString());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate that the old local on-link prefix is still being included
+    // as PIO in the emitted RA.
+
+    sRaValidated = false;
+    sExpectedPio = kNoPio;
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
+    VerifyOrQuit(sDeprecatingPrefixes[0].mPrefix == oldLocalOnLink);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate that Network Data.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Wait for old local on-link prefix to expire.
+
+    while (oldPrefixLifetime > 2 * kMaxRaTxInterval)
+    {
+        // Send same RA from router A to keep its on-link prefix alive.
+
+        SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
+        // Ensure Network Data entries remain as before.
+
+        VerifyExternalRouteInNetData(kDefaultRoute);
+
+        // Keep checking the emitted RAs and make sure the prefix
+        // is included with smaller lifetime every time.
+
+        sRaValidated = false;
+
+        AdvanceTime(kMaxRaTxInterval * 1000);
+
+        VerifyOrQuit(sRaValidated);
+        VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
+        Log("Old on-link prefix is deprecating, remaining lifetime:%d", sDeprecatingPrefixes[0].mLifetime);
+        VerifyOrQuit(sDeprecatingPrefixes[0].mLifetime < oldPrefixLifetime);
+        oldPrefixLifetime = sDeprecatingPrefixes[0].mLifetime;
+    }
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // The old on-link prefix must be expired now and should no
+    // longer be seen in the emitted RA message.
+
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
+    sRaValidated = false;
+
+    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.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
+    // Check behavior when ext PAN ID changes while the local on-link is not
+    // advertised.
+
+    Log("Changing ext PAN ID again");
+
+    oldLocalOnLink = localOnLink;
+
+    sRaValidated = false;
+    sExpectedPio = kNoPio;
+
+    dataset.mExtendedPanId = kExtPanId3;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+
+    AdvanceTime(500);
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+    Log("Local on-link prefix changed to %s from %s", localOnLink.ToString().AsCString(),
+        oldLocalOnLink.ToString().AsCString());
+
+    AdvanceTime(35000);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.IsEmpty());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate the Network Data.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Remove the on-link prefix PIO being advertised by router A
+    // and ensure local on-link prefix is advertised again.
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, 0)});
+
+    AdvanceTime(300000);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.IsEmpty());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Wait for longer than valid lifetime of PIO entry from router A.
+    // Validate that default route is unpublished from network data.
+
+    AdvanceTime(2000 * 1000);
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
+    // Multiple PAN ID changes and multiple deprecating old prefixes.
+
+    oldPrefixes[0] = localOnLink;
+
+    dataset.mExtendedPanId = kExtPanId2;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    AdvanceTime(30000);
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[0]));
+
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Change the prefix again. We should see two deprecating prefixes.
+
+    oldPrefixes[1] = localOnLink;
+
+    dataset.mExtendedPanId = kExtPanId1;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    AdvanceTime(30000);
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 2);
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[0]));
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[1]));
+
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Wait for 15 minutes and then change ext PAN ID again.
+    // Now we should see three deprecating prefixes.
+
+    AdvanceTime(15 * 60 * 1000);
+
+    oldPrefixes[2] = localOnLink;
+
+    dataset.mExtendedPanId = kExtPanId4;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    AdvanceTime(30000);
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 3);
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[0]));
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[1]));
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[2]));
+
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Change ext PAN ID back to previous value of `kExtPanId1`.
+    // We should still see three deprecating prefixes and the last prefix
+    // at `oldPrefixes[2]` should again be treated as local on-link prefix.
+
+    oldPrefixes[3] = localOnLink;
+
+    dataset.mExtendedPanId = kExtPanId1;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    AdvanceTime(30000);
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 3);
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[0]));
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[1]));
+    VerifyOrQuit(oldPrefixes[2] == localOnLink);
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[3]));
+
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Stop BR and validate the final emitted RA to contain
+    // all deprecating prefixes.
+
+    sRaValidated = false;
+    sExpectedPio = kPioDeprecatingLocalOnLink;
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(false));
+    AdvanceTime(100);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 3);
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[0]));
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[1]));
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[3]));
+
+    VerifyNoOmrPrefixInNetData();
+    VerifyExternalRouteInNetData(kNoRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Wait for 15 minutes while BR stays disabled and validate
+    // there are no emitted RAs. We want to check that deprecating
+    // prefixes continue to expire while BR is stopped.
+
+    sRaValidated = false;
+    AdvanceTime(15 * 60 * 1000);
+
+    VerifyOrQuit(!sRaValidated);
+
+    VerifyNoOmrPrefixInNetData();
+    VerifyExternalRouteInNetData(kNoRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start BR again, and check that we only see the last deprecating prefix
+    // at `oldPrefixes[3]` in emitted RA and the other two are expired and
+    // no longer included as PIO and/or in network data.
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(true));
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[3]));
+
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
+    // Validate the oldest prefix is removed when we have too many
+    // back-to-back PAN ID changes.
+
+    // Remember the oldest deprecating prefix (associated with `kExtPanId4`).
+    oldLocalOnLink = oldPrefixes[3];
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(oldPrefixes[0]));
+    dataset.mExtendedPanId = kExtPanId2;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+    AdvanceTime(30000);
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(oldPrefixes[1]));
+    dataset.mExtendedPanId = kExtPanId3;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+    AdvanceTime(30000);
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(oldPrefixes[2]));
+    dataset.mExtendedPanId = kExtPanId5;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+
+    sRaValidated = false;
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRaValidated);
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 3);
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[0]));
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[1]));
+    VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[2]));
+    VerifyOrQuit(!sDeprecatingPrefixes.ContainsMatching(oldLocalOnLink));
+
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    Log("End of TestExtPanIdChange");
+    FinalizeTest();
+}
+
+void TestRouterNsProbe(void)
+{
+    Ip6::Prefix  localOnLink;
+    Ip6::Prefix  localOmr;
+    Ip6::Prefix  onLinkPrefix   = PrefixFromString("2000:abba:baba::", 64);
+    Ip6::Prefix  routePrefix    = PrefixFromString("2000:1234:5678::", 64);
+    Ip6::Address routerAddressA = AddressFromString("fd00::aaaa");
+    Ip6::Address routerAddressB = AddressFromString("fd00::bbbb");
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestRouterNsProbe");
+
+    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");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router A with a new on-link (PIO) and route prefix (RIO).
+
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)},
+                     {Rio(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium)});
+
+    sExpectedPio = kPioDeprecatingLocalOnLink;
+
+    AdvanceTime(10);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check the discovered prefix table and ensure info from router A
+    // is present in the table.
+
+    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, kPreferredLifetime, routerAddressA)},
+                      {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA)});
+
+    AdvanceTime(30000);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router B with same route prefix (RIO) but with
+    // high route preference.
+
+    SendRouterAdvert(routerAddressB, {Rio(routePrefix, kValidLitime, NetworkData::kRoutePreferenceHigh)});
+
+    AdvanceTime(200);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check the discovered prefix table and ensure entries from
+    // both router A and B are seen.
+
+    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, kPreferredLifetime, routerAddressA)},
+                      {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA),
+                       RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceHigh, routerAddressB)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that BR emitted an NS to ensure routers are active.
+
+    sNsEmitted = false;
+    sRsEmitted = false;
+
+    AdvanceTime(160 * 1000);
+
+    VerifyOrQuit(sNsEmitted);
+    VerifyOrQuit(!sRsEmitted);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disallow responding to NS message.
+    //
+    // This should trigger `RoutingManager` to send RS (which will get
+    // no response as well) and then remove all router entries.
+
+    sRespondToNs = false;
+
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sRaValidated = false;
+    sNsEmitted   = false;
+
+    AdvanceTime(240 * 1000);
+
+    VerifyOrQuit(sNsEmitted);
+    VerifyOrQuit(sRaValidated);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check the discovered prefix table. We should see the on-link entry from
+    // router A as deprecated and no route prefix.
+
+    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, 0, routerAddressA)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Verify that no more NS is being sent (since there is no more valid
+    // router entry in the table).
+
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sRaValidated = false;
+    sNsEmitted   = false;
+
+    AdvanceTime(6 * 60 * 1000);
+
+    VerifyOrQuit(!sNsEmitted);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router B and verify that we see router B
+    // entry in prefix table.
+
+    SendRouterAdvert(routerAddressB, {Rio(routePrefix, kValidLitime, NetworkData::kRoutePreferenceHigh)});
+
+    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, 0, routerAddressA)},
+                      {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceHigh, routerAddressB)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Wait for longer than router active time before NS probe.
+    // Check again that NS are sent again.
+
+    sRespondToNs = true;
+    sNsEmitted   = false;
+    sRsEmitted   = false;
+
+    AdvanceTime(3 * 60 * 1000);
+
+    VerifyOrQuit(sNsEmitted);
+    VerifyOrQuit(!sRsEmitted);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    Log("End of TestRouterNsProbe");
+    FinalizeTest();
+}
+
+void TestConflictingPrefix(void)
+{
+    static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x6, 0x7, 0x08}};
+
+    Ip6::Prefix          localOnLink;
+    Ip6::Prefix          oldLocalOnLink;
+    Ip6::Prefix          localOmr;
+    Ip6::Prefix          onLinkPrefix   = PrefixFromString("2000:abba:baba::", 64);
+    Ip6::Address         routerAddressA = AddressFromString("fd00::aaaa");
+    Ip6::Address         routerAddressB = AddressFromString("fd00::bbbb");
+    otOperationalDataset dataset;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestConflictingPrefix");
+
+    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 on-link prefix.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router A with our local on-link prefix as RIO.
+
+    Log("Send RA from router A with local on-link as RIO");
+    SendRouterAdvert(routerAddressA, {Rio(localOnLink, kValidLitime, NetworkData::kRoutePreferenceMedium)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that the local on-link prefix is still being advertised.
+
+    sRaValidated = false;
+    AdvanceTime(610000);
+    VerifyOrQuit(sRaValidated);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data to still include the local OMR and ULA prefix.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router A removing local on-link prefix as RIO.
+
+    SendRouterAdvert(routerAddressA, {Rio(localOnLink, 0, NetworkData::kRoutePreferenceMedium)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // 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);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that the local on-link prefix is still being advertised.
+
+    sRaValidated = false;
+    AdvanceTime(610000);
+    VerifyOrQuit(sRaValidated);
+
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router B advertising an on-link prefix. This
+    // should cause local on-link prefix to be deprecated.
+
+    SendRouterAdvert(routerAddressB, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that the local on-link prefix is now deprecating.
+
+    sRaValidated = false;
+    sExpectedPio = kPioDeprecatingLocalOnLink;
+
+    AdvanceTime(10000);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+    Log("On-link prefix is deprecating, remaining lifetime:%d", sOnLinkLifetime);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data to include the default route now due
+    // the new on-link prefix from router B.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router A again adding local on-link prefix as RIO.
+
+    Log("Send RA from router A with local on-link as RIO");
+    SendRouterAdvert(routerAddressA, {Rio(localOnLink, kValidLitime, NetworkData::kRoutePreferenceMedium)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that the local on-link prefix is still being deprecated.
+
+    sRaValidated = false;
+    AdvanceTime(610000);
+    VerifyOrQuit(sRaValidated);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data remains unchanged.
+
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router A removing the previous RIO.
+
+    SendRouterAdvert(routerAddressA, {Rio(localOnLink, 0, NetworkData::kRoutePreferenceMedium)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data remains unchanged.
+
+    AdvanceTime(60000);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router B removing its on-link prefix.
+
+    SendRouterAdvert(routerAddressB, {Pio(onLinkPrefix, kValidLitime, 0)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that the local on-link prefix is once again being advertised.
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    AdvanceTime(10000);
+    VerifyOrQuit(sRaValidated);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data to remain unchanged.
+
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Change the extended PAN ID.
+
+    Log("Changing ext PAN ID");
+
+    SuccessOrQuit(otDatasetGetActive(sInstance, &dataset));
+
+    VerifyOrQuit(dataset.mComponents.mIsExtendedPanIdPresent);
+
+    dataset.mExtendedPanId = kExtPanId1;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+    AdvanceTime(10000);
+
+    oldLocalOnLink = localOnLink;
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+
+    Log("Local on-link prefix is changed to %s from %s", localOnLink.ToString().AsCString(),
+        oldLocalOnLink.ToString().AsCString());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data contains default route due to the
+    // deprecating on-link prefix from router B.
+
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router A again adding the old local on-link prefix
+    // as RIO.
+
+    SendRouterAdvert(routerAddressA, {Rio(oldLocalOnLink, kValidLitime, NetworkData::kRoutePreferenceMedium)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data remains unchanged.
+
+    AdvanceTime(10000);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send an RA from router A removing the previous RIO.
+
+    SendRouterAdvert(routerAddressA, {Rio(localOnLink, 0, NetworkData::kRoutePreferenceMedium)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data remains unchanged.
+
+    AdvanceTime(10000);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    Log("End of TestConflictingPrefix");
+
+    FinalizeTest();
+}
+
+#if OPENTHREAD_CONFIG_PLATFORM_FLASH_API_ENABLE
+void TestSavedOnLinkPrefixes(void)
+{
+    static const otExtendedPanId kExtPanId1 = {{0x01, 0x02, 0x03, 0x04, 0x05, 0x6, 0x7, 0x08}};
+
+    Ip6::Prefix          localOnLink;
+    Ip6::Prefix          oldLocalOnLink;
+    Ip6::Prefix          localOmr;
+    Ip6::Prefix          onLinkPrefix   = PrefixFromString("2000:abba:baba::", 64);
+    Ip6::Address         routerAddressA = AddressFromString("fd00::aaaa");
+    otOperationalDataset dataset;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestSavedOnLinkPrefixes");
+
+    InitTest(/* aEnablBorderRouting */ true);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check emitted RS and RA messages.
+
+    sRsEmitted   = false;
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Clear();
+
+    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());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data to include the local OMR and ULA prefix.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable the instance and re-enable it.
+
+    Log("Disabling and re-enabling OT Instance");
+
+    testFreeInstance(sInstance);
+
+    InitTest(/* aEnablBorderRouting */ true, /* aAfterReset */ true);
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(true));
+
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRsEmitted);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data to include the local OMR and ULA prefix.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router A advertising an on-link prefix.
+
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
+    sRaValidated = false;
+    sExpectedPio = kPioDeprecatingLocalOnLink;
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 0);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable the instance and re-enable it.
+
+    Log("Disabling and re-enabling OT Instance");
+
+    testFreeInstance(sInstance);
+
+    InitTest(/* aEnablBorderRouting */ true, /* aAfterReset */ true);
+
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRsEmitted);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data to include the local OMR and ULA prefix.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    Log("Changing ext PAN ID");
+
+    oldLocalOnLink = localOnLink;
+
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+
+    SuccessOrQuit(otDatasetGetActive(sInstance, &dataset));
+
+    VerifyOrQuit(dataset.mComponents.mIsExtendedPanIdPresent);
+
+    dataset.mExtendedPanId = kExtPanId1;
+    dataset.mActiveTimestamp.mSeconds++;
+    SuccessOrQuit(otDatasetSetActive(sInstance, &dataset));
+
+    AdvanceTime(30000);
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+    Log("Local on-link prefix changed to %s from %s", localOnLink.ToString().AsCString(),
+        oldLocalOnLink.ToString().AsCString());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable the instance and re-enable it.
+
+    Log("Disabling and re-enabling OT Instance");
+
+    testFreeInstance(sInstance);
+
+    InitTest(/* aEnablBorderRouting */ false, /* aAfterReset */ true);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start Routing Manager.
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(true));
+
+    AdvanceTime(100);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router A advertising an on-link prefix.
+    // This ensures the local on-link prefix is not advertised, but
+    // it must be deprecated since it was advertised last time and
+    // saved in `Settings`.
+
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
+    sRaValidated = false;
+    sExpectedPio = kPioDeprecatingLocalOnLink;
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data to now use default route due to the
+    // on-link prefix from router A.
+
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Wait for more than 1800 seconds to let the deprecating
+    // prefixes expire (keep sending RA from router A).
+
+    for (uint16_t index = 0; index < 185; index++)
+    {
+        SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+        AdvanceTime(10 * 1000);
+    }
+
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable the instance and re-enable it and restart Routing Manager.
+
+    Log("Disabling and re-enabling OT Instance again");
+
+    testFreeInstance(sInstance);
+    InitTest(/* aEnablBorderRouting */ false, /* aAfterReset */ true);
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(true));
+    AdvanceTime(100);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router A advertising an on-link prefix.
+
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
+    sRaValidated = false;
+    sExpectedPio = kNoPio;
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 0);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data still contains the default route.
+
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    Log("End of TestSavedOnLinkPrefixes");
+    FinalizeTest();
+}
+#endif // OPENTHREAD_CONFIG_PLATFORM_FLASH_API_ENABLE
+
+#if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
+void TestAutoEnableOfSrpServer(void)
+{
+    Ip6::Prefix  localOnLink;
+    Ip6::Prefix  localOmr;
+    Ip6::Address routerAddressA = AddressFromString("fd00::aaaa");
+    Ip6::Prefix  onLinkPrefix   = PrefixFromString("2000:abba:baba::", 64);
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestAutoEnableOfSrpServer");
+
+    InitTest();
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check SRP Server state and enable auto-enable mode
+
+    otSrpServerSetAutoEnableMode(sInstance, true);
+    VerifyOrQuit(otSrpServerIsAutoEnableMode(sInstance));
+    VerifyOrQuit(otSrpServerGetState(sInstance) == OT_SRP_SERVER_STATE_DISABLED);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // 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");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate that SRP server was auto-enabled
+
+    VerifyOrQuit(otSrpServerGetState(sInstance) != OT_SRP_SERVER_STATE_DISABLED);
+    Log("Srp::Server is enabled");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Signal that infra if state changed and is no longer running.
+    // This should stop Routing Manager and in turn the SRP server.
+
+    sRaValidated = false;
+    sExpectedPio = kPioDeprecatingLocalOnLink;
+
+    Log("Signal infra if is not running");
+    SuccessOrQuit(otPlatInfraIfStateChanged(sInstance, kInfraIfIndex, false));
+    AdvanceTime(1);
+
+    VerifyOrQuit(sRaValidated);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that SRP server is disabled.
+
+    VerifyOrQuit(otSrpServerGetState(sInstance) == OT_SRP_SERVER_STATE_DISABLED);
+    Log("Srp::Server is disabled");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Signal that infra if state changed and is running again.
+
+    sRsEmitted   = false;
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Add(localOmr);
+
+    Log("Signal infra if is running");
+    SuccessOrQuit(otPlatInfraIfStateChanged(sInstance, kInfraIfIndex, true));
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRsEmitted);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+    Log("Received RA was validated");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that SRP server is enabled again.
+
+    VerifyOrQuit(otSrpServerGetState(sInstance) != OT_SRP_SERVER_STATE_DISABLED);
+    Log("Srp::Server is enabled");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable `RoutingManager` explicitly.
+
+    sRaValidated = false;
+    sExpectedPio = kPioDeprecatingLocalOnLink;
+
+    Log("Disabling RoutingManager");
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(false));
+    AdvanceTime(1);
+
+    VerifyOrQuit(sRaValidated);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that SRP server is also disabled.
+
+    VerifyOrQuit(otSrpServerGetState(sInstance) == OT_SRP_SERVER_STATE_DISABLED);
+    Log("Srp::Server is disabled");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable auto-enable mode on SRP server.
+
+    otSrpServerSetAutoEnableMode(sInstance, false);
+    VerifyOrQuit(!otSrpServerIsAutoEnableMode(sInstance));
+    VerifyOrQuit(otSrpServerGetState(sInstance) == OT_SRP_SERVER_STATE_DISABLED);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Re-start Routing Manager. Check emitted RS and RA messages.
+    // This cycle, router A will send a RA including a PIO.
+
+    sRsEmitted   = false;
+    sRaValidated = false;
+    sExpectedPio = kNoPio;
+    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(2000);
+
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRsEmitted);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+    Log("Received RA was validated");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check that SRP server is still disabled.
+
+    VerifyOrQuit(otSrpServerGetState(sInstance) == OT_SRP_SERVER_STATE_DISABLED);
+    Log("Srp::Server is disabled");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Enable auto-enable mode on SRP server. Since `RoutingManager`
+    // is already done with initial policy evaluation, the SRP server
+    // must be started immediately.
+
+    otSrpServerSetAutoEnableMode(sInstance, true);
+    VerifyOrQuit(otSrpServerIsAutoEnableMode(sInstance));
+
+    VerifyOrQuit(otSrpServerGetState(sInstance) != OT_SRP_SERVER_STATE_DISABLED);
+    Log("Srp::Server is enabled");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable auto-enable mode on SRP server. It must not impact
+    // its current state and it should remain enabled.
+
+    otSrpServerSetAutoEnableMode(sInstance, false);
+    VerifyOrQuit(!otSrpServerIsAutoEnableMode(sInstance));
+
+    AdvanceTime(2000);
+    VerifyOrQuit(otSrpServerGetState(sInstance) != OT_SRP_SERVER_STATE_DISABLED);
+    Log("Srp::Server is enabled");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Signal that infra if state changed and is no longer running.
+    // This should stop Routing Manager.
+
+    sRaValidated = false;
+
+    Log("Signal infra if is not running");
+    SuccessOrQuit(otPlatInfraIfStateChanged(sInstance, kInfraIfIndex, false));
+    AdvanceTime(1);
+
+    VerifyOrQuit(sRaValidated);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Re-enable auto-enable mode on SRP server. Since `RoutingManager`
+    // is stopped (infra if is down), the SRP serer must be stopped
+    // immediately.
+
+    otSrpServerSetAutoEnableMode(sInstance, true);
+    VerifyOrQuit(otSrpServerIsAutoEnableMode(sInstance));
+
+    VerifyOrQuit(otSrpServerGetState(sInstance) == OT_SRP_SERVER_STATE_DISABLED);
+    Log("Srp::Server is disabled");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    Log("End of TestAutoEnableOfSrpServer");
+    FinalizeTest();
+}
+#endif // OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
+
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 
 int main(void)
 {
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
-    TestRoutingManager();
+    TestSamePrefixesFromMultipleRouters();
+    TestOmrSelection();
+    TestDefaultRoute();
+    TestAdvNonUlaRoute();
+    TestLocalOnLinkPrefixDeprecation();
+#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
+    TestDomainPrefixAsOmr();
+#endif
+    TestExtPanIdChange();
+    TestConflictingPrefix();
+    TestRouterNsProbe();
+#if OPENTHREAD_CONFIG_PLATFORM_FLASH_API_ENABLE
+    TestSavedOnLinkPrefixes();
+#endif
+#if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
+    TestAutoEnableOfSrpServer();
+#endif
+
     printf("All tests passed\n");
 #else
     printf("BORDER_ROUTING feature is not enabled\n");
diff --git a/tests/unit/test_serial_number.cpp b/tests/unit/test_serial_number.cpp
index dc8d7a1..df5f118 100644
--- a/tests/unit/test_serial_number.cpp
+++ b/tests/unit/test_serial_number.cpp
@@ -32,7 +32,9 @@
 
 #include "test_util.h"
 #include "common/code_utils.hpp"
+#include "common/num_utils.hpp"
 #include "common/numeric_limits.hpp"
+#include "common/preference.hpp"
 #include "common/serial_number.hpp"
 
 namespace ot {
@@ -65,6 +67,152 @@
     printf("TestSerialNumber<%s>() passed\n", aName);
 }
 
+void TestNumUtils(void)
+{
+    uint16_t u16;
+    uint32_t u32;
+
+    VerifyOrQuit(Min<uint8_t>(1, 2) == 1);
+    VerifyOrQuit(Min<uint8_t>(2, 1) == 1);
+    VerifyOrQuit(Min<uint8_t>(1, 1) == 1);
+
+    VerifyOrQuit(Max<uint8_t>(1, 2) == 2);
+    VerifyOrQuit(Max<uint8_t>(2, 1) == 2);
+    VerifyOrQuit(Max<uint8_t>(1, 1) == 1);
+
+    VerifyOrQuit(Clamp<uint8_t>(1, 5, 10) == 5);
+    VerifyOrQuit(Clamp<uint8_t>(5, 5, 10) == 5);
+    VerifyOrQuit(Clamp<uint8_t>(7, 5, 10) == 7);
+    VerifyOrQuit(Clamp<uint8_t>(10, 5, 10) == 10);
+    VerifyOrQuit(Clamp<uint8_t>(12, 5, 10) == 10);
+
+    VerifyOrQuit(Clamp<uint8_t>(10, 10, 10) == 10);
+    VerifyOrQuit(Clamp<uint8_t>(9, 10, 10) == 10);
+    VerifyOrQuit(Clamp<uint8_t>(11, 10, 10) == 10);
+
+    u16 = 100;
+    VerifyOrQuit(ClampToUint8(u16) == 100);
+    u16 = 255;
+    VerifyOrQuit(ClampToUint8(u16) == 255);
+    u16 = 256;
+    VerifyOrQuit(ClampToUint8(u16) == 255);
+    u16 = 400;
+    VerifyOrQuit(ClampToUint8(u16) == 255);
+
+    u32 = 100;
+    VerifyOrQuit(ClampToUint16(u32) == 100);
+    u32 = 256;
+    VerifyOrQuit(ClampToUint16(u32) == 256);
+    u32 = 0xffff;
+    VerifyOrQuit(ClampToUint16(u32) == 0xffff);
+    u32 = 0x10000;
+    VerifyOrQuit(ClampToUint16(u32) == 0xffff);
+    u32 = 0xfff0000;
+    VerifyOrQuit(ClampToUint16(u32) == 0xffff);
+
+    VerifyOrQuit(ThreeWayCompare<uint8_t>(2, 2) == 0);
+    VerifyOrQuit(ThreeWayCompare<uint8_t>(2, 1) > 0);
+    VerifyOrQuit(ThreeWayCompare<uint8_t>(1, 2) < 0);
+
+    VerifyOrQuit(ThreeWayCompare<bool>(false, false) == 0);
+    VerifyOrQuit(ThreeWayCompare<bool>(true, true) == 0);
+    VerifyOrQuit(ThreeWayCompare<bool>(true, false) > 0);
+    VerifyOrQuit(ThreeWayCompare<bool>(false, true) < 0);
+
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(2, 1) == 2);
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(1, 3) == 0);
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(1, 2) == 1);
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(2, 3) == 1);
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(3, 2) == 2);
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(4, 2) == 2);
+
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(0, 10) == 0);
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(4, 10) == 0);
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(5, 10) == 1);
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(9, 10) == 1);
+    VerifyOrQuit(DivideAndRoundToClosest<uint8_t>(10, 10) == 1);
+
+    VerifyOrQuit(CountBitsInMask<uint8_t>(0) == 0);
+    VerifyOrQuit(CountBitsInMask<uint8_t>(1) == 1);
+    VerifyOrQuit(CountBitsInMask<uint8_t>(2) == 1);
+    VerifyOrQuit(CountBitsInMask<uint8_t>(3) == 2);
+    VerifyOrQuit(CountBitsInMask<uint8_t>(4) == 1);
+    VerifyOrQuit(CountBitsInMask<uint8_t>(7) == 3);
+    VerifyOrQuit(CountBitsInMask<uint8_t>(11) == 3);
+    VerifyOrQuit(CountBitsInMask<uint8_t>(15) == 4);
+    VerifyOrQuit(CountBitsInMask<uint8_t>(0x11) == 2);
+    VerifyOrQuit(CountBitsInMask<uint8_t>(0xef) == 7);
+    VerifyOrQuit(CountBitsInMask<uint8_t>(0xff) == 8);
+
+    VerifyOrQuit(CountBitsInMask<uint16_t>(0) == 0);
+    VerifyOrQuit(CountBitsInMask<uint16_t>(0xff00) == 8);
+    VerifyOrQuit(CountBitsInMask<uint16_t>(0xff) == 8);
+    VerifyOrQuit(CountBitsInMask<uint16_t>(0xaa55) == 8);
+    VerifyOrQuit(CountBitsInMask<uint16_t>(0xffff) == 16);
+
+    printf("TestNumUtils() passed\n");
+}
+
+void TestPreference(void)
+{
+    VerifyOrQuit(Preference::kHigh == 1);
+    VerifyOrQuit(Preference::kMedium == 0);
+    VerifyOrQuit(Preference::kLow == -1);
+
+    // To2BitUint()
+    VerifyOrQuit(Preference::To2BitUint(Preference::kHigh) == 0x1);
+    VerifyOrQuit(Preference::To2BitUint(Preference::kMedium) == 0x0);
+    VerifyOrQuit(Preference::To2BitUint(Preference::kLow) == 0x3);
+    VerifyOrQuit(Preference::To2BitUint(2) == 0x1);
+    VerifyOrQuit(Preference::To2BitUint(-2) == 0x3);
+    VerifyOrQuit(Preference::To2BitUint(127) == 0x1);
+    VerifyOrQuit(Preference::To2BitUint(-128) == 0x3);
+
+    // From2BitUint()
+    VerifyOrQuit(Preference::From2BitUint(0x1) == Preference::kHigh);
+    VerifyOrQuit(Preference::From2BitUint(0x0) == Preference::kMedium);
+    VerifyOrQuit(Preference::From2BitUint(0x3) == Preference::kLow);
+    VerifyOrQuit(Preference::From2BitUint(0x2) == Preference::kMedium);
+
+    VerifyOrQuit(Preference::From2BitUint(0x1 | 4) == Preference::kHigh);
+    VerifyOrQuit(Preference::From2BitUint(0x0 | 4) == Preference::kMedium);
+    VerifyOrQuit(Preference::From2BitUint(0x3 | 4) == Preference::kLow);
+    VerifyOrQuit(Preference::From2BitUint(0x2 | 4) == Preference::kMedium);
+
+    VerifyOrQuit(Preference::From2BitUint(0x1 | 0xfc) == Preference::kHigh);
+    VerifyOrQuit(Preference::From2BitUint(0x0 | 0xfc) == Preference::kMedium);
+    VerifyOrQuit(Preference::From2BitUint(0x3 | 0xfc) == Preference::kLow);
+    VerifyOrQuit(Preference::From2BitUint(0x2 | 0xfc) == Preference::kMedium);
+
+    // IsValid()
+    VerifyOrQuit(Preference::IsValid(Preference::kHigh));
+    VerifyOrQuit(Preference::IsValid(Preference::kMedium));
+    VerifyOrQuit(Preference::IsValid(Preference::kLow));
+
+    VerifyOrQuit(!Preference::IsValid(2));
+    VerifyOrQuit(!Preference::IsValid(-2));
+    VerifyOrQuit(!Preference::IsValid(127));
+    VerifyOrQuit(!Preference::IsValid(-128));
+
+    // Is2BitUintValid
+    VerifyOrQuit(Preference::Is2BitUintValid(0x1));
+    VerifyOrQuit(Preference::Is2BitUintValid(0x0));
+    VerifyOrQuit(Preference::Is2BitUintValid(0x3));
+    VerifyOrQuit(!Preference::Is2BitUintValid(0x2));
+
+    VerifyOrQuit(Preference::Is2BitUintValid(0x1 | 4));
+    VerifyOrQuit(Preference::Is2BitUintValid(0x0 | 4));
+    VerifyOrQuit(Preference::Is2BitUintValid(0x3 | 4));
+    VerifyOrQuit(!Preference::Is2BitUintValid(0x2 | 4));
+
+    VerifyOrQuit(Preference::Is2BitUintValid(0x1 | 0xfc));
+    VerifyOrQuit(Preference::Is2BitUintValid(0x0 | 0xfc));
+    VerifyOrQuit(Preference::Is2BitUintValid(0x3 | 0xfc));
+    VerifyOrQuit(!Preference::Is2BitUintValid(0x2 | 0xfc));
+
+    printf("TestPreference() passed\n");
+}
+
 } // namespace ot
 
 int main(void)
@@ -73,6 +221,8 @@
     ot::TestSerialNumber<uint16_t>("uint16_t");
     ot::TestSerialNumber<uint32_t>("uint32_t");
     ot::TestSerialNumber<uint64_t>("uint64_t");
+    ot::TestNumUtils();
+    ot::TestPreference();
     printf("\nAll tests passed.\n");
     return 0;
 }
diff --git a/tests/unit/test_smart_ptrs.cpp b/tests/unit/test_smart_ptrs.cpp
index 273dc9d..70ec98c 100644
--- a/tests/unit/test_smart_ptrs.cpp
+++ b/tests/unit/test_smart_ptrs.cpp
@@ -60,7 +60,7 @@
 
 template <typename PointerType>
 void VerifyPointer(const PointerType &aPointer,
-                   const TestObject * aObject,
+                   const TestObject  *aObject,
                    uint16_t           aRetainCount = kSkipRetainCountCheck)
 {
     if (aObject == nullptr)
diff --git a/tests/unit/test_spinel_buffer.cpp b/tests/unit/test_spinel_buffer.cpp
index 4df9764..0fbdb07 100644
--- a/tests/unit/test_spinel_buffer.cpp
+++ b/tests/unit/test_spinel_buffer.cpp
@@ -58,7 +58,7 @@
 static const uint8_t sHexText[]        = "0123456789abcdef";
 
 static ot::Instance *sInstance;
-static MessagePool * sMessagePool;
+static MessagePool  *sMessagePool;
 
 struct CallbackContext
 {
@@ -126,10 +126,10 @@
     }
 }
 
-void FrameAddedCallback(void *                   aContext,
+void FrameAddedCallback(void                    *aContext,
                         Spinel::Buffer::FrameTag aTag,
                         Spinel::Buffer::Priority aPriority,
-                        Spinel::Buffer *         aNcpBuffer)
+                        Spinel::Buffer          *aNcpBuffer)
 {
     CallbackContext *callbackContext = reinterpret_cast<CallbackContext *>(aContext);
 
@@ -143,10 +143,10 @@
     callbackContext->mFrameAddedCount++;
 }
 
-void FrameRemovedCallback(void *                   aContext,
+void FrameRemovedCallback(void                    *aContext,
                           Spinel::Buffer::FrameTag aTag,
                           Spinel::Buffer::Priority aPriority,
-                          Spinel::Buffer *         aNcpBuffer)
+                          Spinel::Buffer          *aNcpBuffer)
 {
     CallbackContext *callbackContext = reinterpret_cast<CallbackContext *>(aContext);
 
@@ -173,7 +173,7 @@
 
 void WriteTestFrame1(Spinel::Buffer &aNcpBuffer, Spinel::Buffer::Priority aPriority)
 {
-    Message *       message;
+    Message        *message;
     CallbackContext oldContext;
 
     message = sMessagePool->Allocate(Message::kTypeIp6);
@@ -216,8 +216,8 @@
 
 void WriteTestFrame2(Spinel::Buffer &aNcpBuffer, Spinel::Buffer::Priority aPriority)
 {
-    Message *       message1;
-    Message *       message2;
+    Message        *message1;
+    Message        *message2;
     CallbackContext oldContext = sContext;
 
     message1 = sMessagePool->Allocate(Message::kTypeIp6);
@@ -262,7 +262,7 @@
 
 void WriteTestFrame3(Spinel::Buffer &aNcpBuffer, Spinel::Buffer::Priority aPriority)
 {
-    Message *       message1;
+    Message        *message1;
     CallbackContext oldContext = sContext;
 
     message1 = sMessagePool->Allocate(Message::kTypeIp6);
@@ -335,7 +335,7 @@
     uint8_t        buffer[kTestBufferSize];
     Spinel::Buffer ncpBuffer(buffer, kTestBufferSize);
 
-    Message *                     message;
+    Message                      *message;
     uint8_t                       readBuffer[16];
     uint16_t                      readLen, readOffset;
     Spinel::Buffer::WritePosition pos1, pos2;
diff --git a/tests/unit/test_spinel_decoder.cpp b/tests/unit/test_spinel_decoder.cpp
index 13bd82c..1a2249a 100644
--- a/tests/unit/test_spinel_decoder.cpp
+++ b/tests/unit/test_spinel_decoder.cpp
@@ -90,12 +90,12 @@
     int64_t                  i64;
     unsigned int             u_1, u_2, u_3, u_4;
     const spinel_ipv6addr_t *ip6Addr;
-    const spinel_eui48_t *   eui48;
-    const spinel_eui64_t *   eui64;
-    const char *             utf_1;
-    const char *             utf_2;
-    const uint8_t *          dataPtr_1;
-    const uint8_t *          dataPtr_2;
+    const spinel_eui48_t    *eui48;
+    const spinel_eui64_t    *eui64;
+    const char              *utf_1;
+    const char              *utf_2;
+    const uint8_t           *dataPtr_1;
+    const uint8_t           *dataPtr_2;
     uint16_t                 dataLen_1;
     uint16_t                 dataLen_2;
 
diff --git a/tests/unit/test_spinel_encoder.cpp b/tests/unit/test_spinel_encoder.cpp
index b640af3..7acb0d0 100644
--- a/tests/unit/test_spinel_encoder.cpp
+++ b/tests/unit/test_spinel_encoder.cpp
@@ -105,11 +105,11 @@
     int64_t            i64;
     unsigned int       u_1, u_2, u_3, u_4;
     spinel_ipv6addr_t *ip6Addr;
-    spinel_eui48_t *   eui48;
-    spinel_eui64_t *   eui64;
-    const char *       utf_1;
-    const char *       utf_2;
-    const uint8_t *    dataPtr;
+    spinel_eui48_t    *eui48;
+    spinel_eui64_t    *eui64;
+    const char        *utf_1;
+    const char        *utf_2;
+    const uint8_t     *dataPtr;
     spinel_size_t      dataLen;
 
     memset(buffer, 0, sizeof(buffer));
@@ -145,7 +145,7 @@
     DumpBuffer("Frame", frame, frameLen);
 
     parsedLen = spinel_datatype_unpack(
-        frame, (spinel_size_t)frameLen,
+        frame, static_cast<spinel_size_t>(frameLen),
         (SPINEL_DATATYPE_BOOL_S SPINEL_DATATYPE_BOOL_S SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_INT8_S
              SPINEL_DATATYPE_UINT16_S SPINEL_DATATYPE_INT16_S SPINEL_DATATYPE_UINT32_S SPINEL_DATATYPE_INT32_S
                  SPINEL_DATATYPE_UINT64_S SPINEL_DATATYPE_INT64_S SPINEL_DATATYPE_UINT_PACKED_S
@@ -200,7 +200,7 @@
     DumpBuffer("Frame", frame, frameLen);
 
     parsedLen = spinel_datatype_unpack(
-        frame, (spinel_size_t)frameLen,
+        frame, static_cast<spinel_size_t>(frameLen),
         (SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_STRUCT_S(
             SPINEL_DATATYPE_UINT32_S SPINEL_DATATYPE_EUI48_S SPINEL_DATATYPE_UINT_PACKED_S) SPINEL_DATATYPE_INT16_S
 
@@ -215,7 +215,7 @@
     VerifyOrQuit(memcmp(eui48, &kEui48, sizeof(spinel_eui48_t)) == 0);
 
     // Parse the struct as a "data with len".
-    parsedLen = spinel_datatype_unpack(frame, (spinel_size_t)frameLen,
+    parsedLen = spinel_datatype_unpack(frame, static_cast<spinel_size_t>(frameLen),
                                        (SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_DATA_WLEN_S SPINEL_DATATYPE_INT16_S
 
                                         ),
@@ -258,7 +258,7 @@
     SuccessOrQuit(ReadFrame(ncpBuffer, frame, frameLen));
 
     parsedLen = spinel_datatype_unpack(
-        frame, (spinel_size_t)frameLen,
+        frame, static_cast<spinel_size_t>(frameLen),
         (SPINEL_DATATYPE_STRUCT_S(SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_UTF8_S SPINEL_DATATYPE_STRUCT_S(
             SPINEL_DATATYPE_BOOL_S SPINEL_DATATYPE_IPv6ADDR_S) SPINEL_DATATYPE_UINT16_S)
              SPINEL_DATATYPE_EUI48_S SPINEL_DATATYPE_STRUCT_S(SPINEL_DATATYPE_UINT32_S) SPINEL_DATATYPE_INT32_S),
@@ -296,7 +296,7 @@
     SuccessOrQuit(ReadFrame(ncpBuffer, frame, frameLen));
 
     parsedLen = spinel_datatype_unpack(
-        frame, (spinel_size_t)frameLen,
+        frame, static_cast<spinel_size_t>(frameLen),
         (SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_STRUCT_S(
             SPINEL_DATATYPE_UINT32_S SPINEL_DATATYPE_STRUCT_S(SPINEL_DATATYPE_EUI48_S SPINEL_DATATYPE_UINT_PACKED_S))),
         &u8, &u32, &eui48, &u_3);
@@ -340,7 +340,7 @@
     SuccessOrQuit(ReadFrame(ncpBuffer, frame, frameLen));
 
     parsedLen = spinel_datatype_unpack(
-        frame, (spinel_size_t)frameLen,
+        frame, static_cast<spinel_size_t>(frameLen),
         (SPINEL_DATATYPE_UINT8_S SPINEL_DATATYPE_STRUCT_S(
             SPINEL_DATATYPE_UINT32_S SPINEL_DATATYPE_IPv6ADDR_S SPINEL_DATATYPE_EUI64_S) SPINEL_DATATYPE_UTF8_S),
         &u8, &u32, &ip6Addr, &eui64, &utf_1);
diff --git a/tests/unit/test_srp_server.cpp b/tests/unit/test_srp_server.cpp
new file mode 100644
index 0000000..34ab905
--- /dev/null
+++ b/tests/unit/test_srp_server.cpp
@@ -0,0 +1,919 @@
+/*
+ *  Copyright (c) 2022, 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/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_SRP_SERVER_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE && \
+    !OPENTHREAD_CONFIG_TIME_SYNC_ENABLE && !OPENTHREAD_PLATFORM_POSIX
+#define ENABLE_SRP_TEST 1
+#else
+#define ENABLE_SRP_TEST 0
+#endif
+
+#if ENABLE_SRP_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));
+    SuccessOrQuit(otInstanceErasePersistentInfo(sInstance));
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+enum UpdateHandlerMode
+{
+    kAccept, // Accept all updates.
+    kReject, // Reject all updates.
+    kIgnore  // Ignore all updates (do not call `otSrpServerHandleServiceUpdateResult()`).
+};
+
+static UpdateHandlerMode    sUpdateHandlerMode       = kAccept;
+static bool                 sProcessedUpdateCallback = false;
+static otSrpServerLeaseInfo sUpdateHostLeaseInfo;
+static uint32_t             sUpdateHostKeyLease;
+
+void HandleSrpServerUpdate(otSrpServerServiceUpdateId aId,
+                           const otSrpServerHost     *aHost,
+                           uint32_t                   aTimeout,
+                           void                      *aContext)
+{
+    Log("HandleSrpServerUpdate() called with %u, timeout:%u", aId, aTimeout);
+
+    VerifyOrQuit(aHost != nullptr);
+    VerifyOrQuit(aContext == sInstance);
+
+    sProcessedUpdateCallback = true;
+
+    otSrpServerHostGetLeaseInfo(aHost, &sUpdateHostLeaseInfo);
+
+    switch (sUpdateHandlerMode)
+    {
+    case kAccept:
+        otSrpServerHandleServiceUpdateResult(sInstance, aId, kErrorNone);
+        break;
+    case kReject:
+        otSrpServerHandleServiceUpdateResult(sInstance, aId, kErrorFailed);
+        break;
+    case kIgnore:
+        break;
+    }
+}
+
+static bool  sProcessedClientCallback = false;
+static Error sLastClientCallbackError = kErrorNone;
+
+void HandleSrpClientCallback(otError                    aError,
+                             const otSrpClientHostInfo *aHostInfo,
+                             const otSrpClientService  *aServices,
+                             const otSrpClientService  *aRemovedServices,
+                             void                      *aContext)
+{
+    Log("HandleSrpClientCallback() called with error %s", ErrorToString(aError));
+
+    VerifyOrQuit(aContext == sInstance);
+
+    sProcessedClientCallback = true;
+    sLastClientCallbackError = aError;
+
+    OT_UNUSED_VARIABLE(aHostInfo);
+    OT_UNUSED_VARIABLE(aServices);
+    OT_UNUSED_VARIABLE(aRemovedServices);
+}
+
+static const char kHostName[] = "myhost";
+
+void PrepareService1(Srp::Client::Service &aService)
+{
+    static const char          kServiceName[]   = "_srv._udp";
+    static const char          kInstanceLabel[] = "srv-instance";
+    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          = kServiceName;
+    aService.mInstanceName  = kInstanceLabel;
+    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  kService2Name[]   = "_00112233667882554._matter._udp";
+    static const char  kInstance2Label[] = "ABCDEFGHI";
+    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 TestSrpServerBase(void)
+{
+    Srp::Server         *srpServer;
+    Srp::Client         *srpClient;
+    Srp::Client::Service service1;
+    Srp::Client::Service service2;
+    uint16_t             heapAllocations;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestSrpServerBase");
+
+    InitTest();
+
+    srpServer = &sInstance->Get<Srp::Server>();
+    srpClient = &sInstance->Get<Srp::Client>();
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+
+    PrepareService1(service1);
+    PrepareService2(service2);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start SRP server.
+
+    SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast));
+    VerifyOrQuit(srpServer->GetAddressMode() == Srp::Server::kAddressModeUnicast);
+
+    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled);
+
+    srpServer->SetServiceHandler(HandleSrpServerUpdate, sInstance);
+
+    srpServer->SetEnabled(true);
+    VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled);
+
+    AdvanceTime(10000);
+    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start SRP client.
+
+    srpClient->SetCallback(HandleSrpClientCallback, sInstance);
+
+    srpClient->EnableAutoStartMode(nullptr, nullptr);
+    VerifyOrQuit(srpClient->IsAutoStartModeEnabled());
+
+    AdvanceTime(2000);
+    VerifyOrQuit(srpClient->IsRunning());
+
+    SuccessOrQuit(srpClient->SetHostName(kHostName));
+    SuccessOrQuit(srpClient->EnableAutoHostAddress());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Register a service, validate that update handler is called.
+
+    SuccessOrQuit(srpClient->AddService(service1));
+
+    sUpdateHandlerMode       = kAccept;
+    sProcessedUpdateCallback = false;
+    sProcessedClientCallback = false;
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(sProcessedUpdateCallback);
+    VerifyOrQuit(sProcessedClientCallback);
+    VerifyOrQuit(sLastClientCallbackError == kErrorNone);
+
+    VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered);
+    ValidateHost(*srpServer, kHostName);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Register a second service, validate that update handler is called.
+
+    SuccessOrQuit(srpClient->AddService(service2));
+
+    sProcessedUpdateCallback = false;
+    sProcessedClientCallback = false;
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(sProcessedUpdateCallback);
+    VerifyOrQuit(sProcessedClientCallback);
+    VerifyOrQuit(sLastClientCallbackError == kErrorNone);
+
+    VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered);
+    VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Unregister first service, validate that update handler is called.
+
+    SuccessOrQuit(srpClient->RemoveService(service1));
+
+    sProcessedUpdateCallback = false;
+    sProcessedClientCallback = false;
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(sProcessedUpdateCallback);
+    VerifyOrQuit(sProcessedClientCallback);
+    VerifyOrQuit(sLastClientCallbackError == kErrorNone);
+
+    VerifyOrQuit(service1.GetState() == Srp::Client::kRemoved);
+    VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable SRP server, verify that all heap allocations by SRP server
+    // 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 TestSrpServerBase");
+}
+
+void TestSrpServerReject(void)
+{
+    Srp::Server         *srpServer;
+    Srp::Client         *srpClient;
+    Srp::Client::Service service1;
+    Srp::Client::Service service2;
+    uint16_t             heapAllocations;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestSrpServerReject");
+
+    InitTest();
+
+    srpServer = &sInstance->Get<Srp::Server>();
+    srpClient = &sInstance->Get<Srp::Client>();
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+
+    PrepareService1(service1);
+    PrepareService2(service2);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start SRP server.
+
+    SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast));
+    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled);
+
+    srpServer->SetServiceHandler(HandleSrpServerUpdate, sInstance);
+
+    srpServer->SetEnabled(true);
+    VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled);
+
+    AdvanceTime(10000);
+    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start SRP client.
+
+    srpClient->SetCallback(HandleSrpClientCallback, sInstance);
+
+    srpClient->EnableAutoStartMode(nullptr, nullptr);
+    VerifyOrQuit(srpClient->IsAutoStartModeEnabled());
+
+    AdvanceTime(2000);
+    VerifyOrQuit(srpClient->IsRunning());
+
+    SuccessOrQuit(srpClient->SetHostName(kHostName));
+    SuccessOrQuit(srpClient->EnableAutoHostAddress());
+
+    sUpdateHandlerMode = kReject;
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Register a service, validate that update handler is called
+    // and rejected and no service is registered.
+
+    SuccessOrQuit(srpClient->AddService(service1));
+
+    sProcessedUpdateCallback = false;
+    sProcessedClientCallback = false;
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(sProcessedUpdateCallback);
+    VerifyOrQuit(sProcessedClientCallback);
+    VerifyOrQuit(sLastClientCallbackError != kErrorNone);
+
+    VerifyOrQuit(service1.GetState() != Srp::Client::kRegistered);
+
+    VerifyOrQuit(srpServer->GetNextHost(nullptr) == nullptr);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Register a second service, validate that update handler is
+    // again called and update is rejected.
+
+    SuccessOrQuit(srpClient->AddService(service2));
+
+    sProcessedUpdateCallback = false;
+    sProcessedClientCallback = false;
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(sProcessedUpdateCallback);
+    VerifyOrQuit(sProcessedClientCallback);
+    VerifyOrQuit(sLastClientCallbackError != kErrorNone);
+
+    VerifyOrQuit(service1.GetState() != Srp::Client::kRegistered);
+    VerifyOrQuit(service2.GetState() != Srp::Client::kRegistered);
+
+    VerifyOrQuit(srpServer->GetNextHost(nullptr) == nullptr);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable SRP server, verify that all heap allocations by SRP server
+    // 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 TestSrpServerReject");
+}
+
+void TestSrpServerIgnore(void)
+{
+    Srp::Server         *srpServer;
+    Srp::Client         *srpClient;
+    Srp::Client::Service service1;
+    Srp::Client::Service service2;
+    uint16_t             heapAllocations;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestSrpServerIgnore");
+
+    InitTest();
+
+    srpServer = &sInstance->Get<Srp::Server>();
+    srpClient = &sInstance->Get<Srp::Client>();
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+
+    PrepareService1(service1);
+    PrepareService2(service2);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start SRP server.
+
+    SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast));
+    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled);
+
+    srpServer->SetServiceHandler(HandleSrpServerUpdate, sInstance);
+
+    srpServer->SetEnabled(true);
+    VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled);
+
+    AdvanceTime(10000);
+    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start SRP client.
+
+    srpClient->SetCallback(HandleSrpClientCallback, sInstance);
+
+    srpClient->EnableAutoStartMode(nullptr, nullptr);
+    VerifyOrQuit(srpClient->IsAutoStartModeEnabled());
+
+    AdvanceTime(2000);
+    VerifyOrQuit(srpClient->IsRunning());
+
+    SuccessOrQuit(srpClient->SetHostName(kHostName));
+    SuccessOrQuit(srpClient->EnableAutoHostAddress());
+
+    sUpdateHandlerMode = kIgnore;
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Register a service, validate that update handler is called
+    // and ignored the update and no service is registered.
+
+    SuccessOrQuit(srpClient->AddService(service1));
+
+    sProcessedUpdateCallback = false;
+    sProcessedClientCallback = false;
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(sProcessedUpdateCallback);
+    VerifyOrQuit(sProcessedClientCallback);
+    VerifyOrQuit(sLastClientCallbackError != kErrorNone);
+
+    VerifyOrQuit(service1.GetState() != Srp::Client::kRegistered);
+
+    VerifyOrQuit(srpServer->GetNextHost(nullptr) == nullptr);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Register a second service, validate that update handler is
+    // again called and update is still ignored.
+
+    SuccessOrQuit(srpClient->AddService(service2));
+
+    sProcessedUpdateCallback = false;
+    sProcessedClientCallback = false;
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(sProcessedUpdateCallback);
+    VerifyOrQuit(sProcessedClientCallback);
+    VerifyOrQuit(sLastClientCallbackError != kErrorNone);
+
+    VerifyOrQuit(service1.GetState() != Srp::Client::kRegistered);
+    VerifyOrQuit(service2.GetState() != Srp::Client::kRegistered);
+
+    VerifyOrQuit(srpServer->GetNextHost(nullptr) == nullptr);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable SRP server, verify that all heap allocations by SRP server
+    // 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 TestSrpServerIgnore");
+}
+
+#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+void TestUpdateLeaseShortVariant(void)
+{
+    // Test behavior of SRP client and server when short variant of
+    // Update Lease Option is used (which only include lease interval).
+    // This test uses `SetUseShortLeaseOption()` method of `Srp::Client`
+    // which changes the default behavior and is available under the
+    // `REFERENCE_DEVICE` config.
+
+    Srp::Server                *srpServer;
+    Srp::Server::LeaseConfig    leaseConfig;
+    const Srp::Server::Service *service;
+    Srp::Client                *srpClient;
+    Srp::Client::Service        service1;
+    uint16_t                    heapAllocations;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestUpdateLeaseShortVariant");
+
+    InitTest();
+
+    srpServer = &sInstance->Get<Srp::Server>();
+    srpClient = &sInstance->Get<Srp::Client>();
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+
+    PrepareService1(service1);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start SRP server.
+
+    SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast));
+    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled);
+
+    srpServer->SetServiceHandler(HandleSrpServerUpdate, sInstance);
+
+    srpServer->SetEnabled(true);
+    VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled);
+
+    AdvanceTime(10000);
+    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check the default Lease Config on SRP server.
+    // Server to accept lease in [30 sec, 27 hours] and
+    // key-lease in [30 sec, 189 hours].
+
+    srpServer->GetLeaseConfig(leaseConfig);
+
+    VerifyOrQuit(leaseConfig.mMinLease == 30);             // 30 seconds
+    VerifyOrQuit(leaseConfig.mMaxLease == 27u * 3600);     // 27 hours
+    VerifyOrQuit(leaseConfig.mMinKeyLease == 30);          // 30 seconds
+    VerifyOrQuit(leaseConfig.mMaxKeyLease == 189u * 3600); // 189 hours
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start SRP client.
+
+    srpClient->SetCallback(HandleSrpClientCallback, sInstance);
+
+    srpClient->EnableAutoStartMode(nullptr, nullptr);
+    VerifyOrQuit(srpClient->IsAutoStartModeEnabled());
+
+    AdvanceTime(2000);
+    VerifyOrQuit(srpClient->IsRunning());
+
+    SuccessOrQuit(srpClient->SetHostName(kHostName));
+    SuccessOrQuit(srpClient->EnableAutoHostAddress());
+
+    sUpdateHandlerMode = kAccept;
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Change default lease intervals on SRP client and enable
+    // "use short Update Lease Option" mode.
+
+    srpClient->SetLeaseInterval(15u * 3600);
+    srpClient->SetKeyLeaseInterval(40u * 3600);
+
+    srpClient->SetUseShortLeaseOption(true);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Register a service, validate that update handler is called
+    // and service is successfully registered.
+
+    SuccessOrQuit(srpClient->AddService(service1));
+
+    sProcessedUpdateCallback = false;
+    sProcessedClientCallback = false;
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(sProcessedUpdateCallback);
+    VerifyOrQuit(sProcessedClientCallback);
+    VerifyOrQuit(sLastClientCallbackError == kErrorNone);
+
+    VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered);
+
+    ValidateHost(*srpServer, kHostName);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate the lease info for service on SRP server. The client
+    // is set up to use "short Update Lease Option format, so it only
+    // include the lease interval as 15 hours in its request
+    // message. Server should then see 15 hours for both lease and
+    // key lease
+
+    VerifyOrQuit(sUpdateHostLeaseInfo.mLease == 15u * 3600 * 1000);
+    VerifyOrQuit(sUpdateHostLeaseInfo.mKeyLease == 15u * 3600 * 1000);
+
+    // Check that SRP server granted 15 hours for both lease and
+    // key lease.
+
+    service = srpServer->GetNextHost(nullptr)->GetServices().GetHead();
+    VerifyOrQuit(service != nullptr);
+    VerifyOrQuit(service->GetLease() == 15u * 3600);
+    VerifyOrQuit(service->GetKeyLease() == 15u * 3600);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Remove the service.
+
+    SuccessOrQuit(srpClient->RemoveService(service1));
+
+    sProcessedUpdateCallback = false;
+    sProcessedClientCallback = false;
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(sProcessedUpdateCallback);
+    VerifyOrQuit(sProcessedClientCallback);
+    VerifyOrQuit(sLastClientCallbackError == kErrorNone);
+
+    VerifyOrQuit(service1.GetState() == Srp::Client::kRemoved);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Register the service again, but this time change it to request
+    // a lease time that is larger than the `LeaseConfig.mMinLease` of
+    // 27 hours. This ensures that server needs to include the Lease
+    // Option in its response (since it need to grant a different
+    // lease interval).
+
+    service1.mLease    = 100u * 3600; // 100 hours >= 27 hours.
+    service1.mKeyLease = 110u * 3600;
+
+    SuccessOrQuit(srpClient->AddService(service1));
+
+    sProcessedUpdateCallback = false;
+    sProcessedClientCallback = false;
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(sProcessedUpdateCallback);
+    VerifyOrQuit(sProcessedClientCallback);
+    VerifyOrQuit(sLastClientCallbackError == kErrorNone);
+
+    VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered);
+
+    ValidateHost(*srpServer, kHostName);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate the lease info for service on SRP server.
+
+    // We should see the 100 hours in request from client
+    VerifyOrQuit(sUpdateHostLeaseInfo.mLease == 100u * 3600 * 1000);
+    VerifyOrQuit(sUpdateHostLeaseInfo.mKeyLease == 100u * 3600 * 1000);
+
+    // Check that SRP server granted  27 hours for both lease and
+    // key lease.
+
+    service = srpServer->GetNextHost(nullptr)->GetServices().GetHead();
+    VerifyOrQuit(service != nullptr);
+    VerifyOrQuit(service->GetLease() == 27u * 3600);
+    VerifyOrQuit(service->GetKeyLease() == 27u * 3600);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable SRP server, verify that all heap allocations by SRP server
+    // 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 TestUpdateLeaseShortVariant");
+}
+
+#endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+
+#endif // ENABLE_SRP_TEST
+
+int main(void)
+{
+#if ENABLE_SRP_TEST
+    TestSrpServerBase();
+    TestSrpServerReject();
+    TestSrpServerIgnore();
+#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+    TestUpdateLeaseShortVariant();
+#endif
+    printf("All tests passed\n");
+#else
+    printf("SRP_SERVER or SRP_CLIENT feature is not enabled\n");
+#endif
+
+    return 0;
+}
diff --git a/tests/unit/test_string.cpp b/tests/unit/test_string.cpp
index b32a111..80ff3b0 100644
--- a/tests/unit/test_string.cpp
+++ b/tests/unit/test_string.cpp
@@ -309,12 +309,72 @@
     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");
 static_assert(ot::AreStringsInOrder("aa", "aaa"), "AreStringsInOrder() failed");
 static_assert(ot::AreStringsInOrder("", "a"), "AreStringsInOrder() failed");
 static_assert(!ot::AreStringsInOrder("cd", "cd"), "AreStringsInOrder() failed");
 static_assert(!ot::AreStringsInOrder("z", "abcd"), "AreStringsInOrder() failed");
 static_assert(!ot::AreStringsInOrder("0", ""), "AreStringsInOrder() failed");
+#endif
 
 } // namespace ot
 
@@ -328,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 00cc02e..b6ecd66 100644
--- a/tests/unit/test_timer.cpp
+++ b/tests/unit/test_timer.cpp
@@ -65,10 +65,7 @@
     sPlatDt = aDt;
 }
 
-uint32_t otPlatAlarmMilliGetNow(void)
-{
-    return sNow;
-}
+uint32_t otPlatAlarmMilliGetNow(void) { return sNow; }
 
 #if OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
 void otPlatAlarmMicroStop(otInstance *)
@@ -85,18 +82,12 @@
     sPlatDt = aDt;
 }
 
-uint32_t otPlatAlarmMicroGetNow(void)
-{
-    return sNow;
-}
+uint32_t otPlatAlarmMicroGetNow(void) { return sNow; }
 #endif
 
 } // extern "C"
 
-void InitCounters(void)
-{
-    memset(sCallCount, 0, sizeof(sCallCount));
-}
+void InitCounters(void) { memset(sCallCount, 0, sizeof(sCallCount)); }
 
 /**
  * `TestTimer` sub-classes `ot::TimerMilli` and provides a handler and a counter to keep track of number of times timer
@@ -131,16 +122,10 @@
 
 template <typename TimerType> void AlarmFired(otInstance *aInstance);
 
-template <> void AlarmFired<ot::TimerMilli>(otInstance *aInstance)
-{
-    otPlatAlarmMilliFired(aInstance);
-}
+template <> void AlarmFired<ot::TimerMilli>(otInstance *aInstance) { otPlatAlarmMilliFired(aInstance); }
 
 #if OPENTHREAD_CONFIG_PLATFORM_USEC_TIMER_ENABLE
-template <> void AlarmFired<ot::TimerMicro>(otInstance *aInstance)
-{
-    otPlatAlarmMicroFired(aInstance);
-}
+template <> void AlarmFired<ot::TimerMicro>(otInstance *aInstance) { otPlatAlarmMicroFired(aInstance); }
 #endif
 
 /**
@@ -150,7 +135,7 @@
 {
     const uint32_t       kTimeT0        = 1000;
     const uint32_t       kTimerInterval = 10;
-    ot::Instance *       instance       = testInitInstance();
+    ot::Instance        *instance       = testInitInstance();
     TestTimer<TimerType> timer(*instance);
 
     // Test one Timer basic operation.
@@ -276,7 +261,7 @@
 {
     const uint32_t       kTimeT0        = 1000;
     const uint32_t       kTimerInterval = 10;
-    ot::Instance *       instance       = testInitInstance();
+    ot::Instance        *instance       = testInitInstance();
     TestTimer<TimerType> timer1(*instance);
     TestTimer<TimerType> timer2(*instance);
 
diff --git a/tests/unit/test_tlv.cpp b/tests/unit/test_tlv.cpp
new file mode 100644
index 0000000..1af0121
--- /dev/null
+++ b/tests/unit/test_tlv.cpp
@@ -0,0 +1,194 @@
+/*
+ *  Copyright (c) 2022, 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 "test_platform.h"
+
+#include <openthread/config.h>
+
+#include "common/instance.hpp"
+#include "common/message.hpp"
+#include "common/tlvs.hpp"
+
+#include "test_util.h"
+
+namespace ot {
+
+void TestTlv(void)
+{
+    Instance   *instance = testInitInstance();
+    Message    *message;
+    Tlv         tlv;
+    ExtendedTlv extTlv;
+    uint16_t    offset;
+    uint16_t    valueOffset;
+    uint16_t    length;
+    uint8_t     buffer[4];
+
+    VerifyOrQuit(instance != nullptr);
+
+    VerifyOrQuit((message = instance->Get<MessagePool>().Allocate(Message::kTypeIp6)) != nullptr);
+    VerifyOrQuit(message != nullptr);
+
+    VerifyOrQuit(message->GetOffset() == 0);
+    VerifyOrQuit(message->GetLength() == 0);
+
+    VerifyOrQuit(Tlv::FindTlvValueOffset(*message, /* aType */ 1, valueOffset, length) == kErrorNotFound);
+    VerifyOrQuit(Tlv::ReadTlvValue(*message, 0, buffer, 1) == kErrorParse);
+
+    // Add an empty TLV with type 1 and check that we can find it
+
+    offset = message->GetLength();
+
+    tlv.SetType(1);
+    tlv.SetLength(0);
+    SuccessOrQuit(message->Append(tlv));
+
+    SuccessOrQuit(Tlv::FindTlvValueOffset(*message, /* aType */ 1, valueOffset, length));
+    VerifyOrQuit(valueOffset == sizeof(Tlv));
+    VerifyOrQuit(length == 0);
+    SuccessOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 0));
+    VerifyOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 1) == kErrorParse);
+
+    // Add an empty extended TLV (type 2), and check that we can find it.
+
+    offset = message->GetLength();
+
+    extTlv.SetType(2);
+    extTlv.SetLength(0);
+    SuccessOrQuit(message->Append(extTlv));
+
+    SuccessOrQuit(Tlv::FindTlvValueOffset(*message, /* aType */ 2, valueOffset, length));
+    VerifyOrQuit(valueOffset == offset + sizeof(ExtendedTlv));
+    VerifyOrQuit(length == 0);
+    SuccessOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 0));
+    VerifyOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 1) == kErrorParse);
+
+    // Add a TLV with type 3 with one byte value and check if we can find it.
+
+    offset = message->GetLength();
+
+    tlv.SetType(3);
+    tlv.SetLength(1);
+    SuccessOrQuit(message->Append(tlv));
+    SuccessOrQuit(message->Append<uint8_t>(0xff));
+
+    SuccessOrQuit(Tlv::FindTlvValueOffset(*message, /* aType */ 3, valueOffset, length));
+    VerifyOrQuit(valueOffset == offset + sizeof(Tlv));
+    VerifyOrQuit(length == 1);
+    SuccessOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 1));
+    VerifyOrQuit(buffer[0] == 0x0ff);
+    VerifyOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 2) == kErrorParse);
+
+    // Add an extended TLV with type 4 with two byte value and check if we can find it.
+
+    offset = message->GetLength();
+
+    extTlv.SetType(4);
+    extTlv.SetLength(2);
+    SuccessOrQuit(message->Append(extTlv));
+    SuccessOrQuit(message->Append<uint8_t>(0x12));
+    SuccessOrQuit(message->Append<uint8_t>(0x34));
+
+    SuccessOrQuit(Tlv::FindTlvValueOffset(*message, /* aType */ 4, valueOffset, length));
+    VerifyOrQuit(valueOffset == offset + sizeof(ExtendedTlv));
+    VerifyOrQuit(length == 2);
+    SuccessOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 1));
+    VerifyOrQuit(buffer[0] == 0x12);
+    SuccessOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 2));
+    VerifyOrQuit(buffer[0] == 0x12);
+    VerifyOrQuit(buffer[1] == 0x34);
+    VerifyOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 3) == kErrorParse);
+
+    // Add a TLV with missing value.
+
+    offset = message->GetLength();
+
+    tlv.SetType(5);
+    tlv.SetLength(1);
+    SuccessOrQuit(message->Append(tlv));
+
+    VerifyOrQuit(Tlv::FindTlvValueOffset(*message, /* aType */ 5, valueOffset, length) != kErrorNone);
+    VerifyOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 0) == kErrorParse);
+
+    // Add the missing value.
+    SuccessOrQuit(message->Append<uint8_t>(0xaa));
+
+    SuccessOrQuit(Tlv::FindTlvValueOffset(*message, /* aType */ 5, valueOffset, length));
+    VerifyOrQuit(valueOffset == offset + sizeof(Tlv));
+    VerifyOrQuit(length == 1);
+    SuccessOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 1));
+    VerifyOrQuit(buffer[0] == 0xaa);
+    VerifyOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 2) == kErrorParse);
+
+    // Add an extended TLV with missing value.
+
+    offset = message->GetLength();
+
+    extTlv.SetType(6);
+    extTlv.SetLength(2);
+    SuccessOrQuit(message->Append(extTlv));
+    SuccessOrQuit(message->Append<uint8_t>(0xbb));
+
+    VerifyOrQuit(Tlv::FindTlvValueOffset(*message, /* aType */ 6, valueOffset, length) != kErrorNone);
+    VerifyOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 1) == kErrorParse);
+
+    SuccessOrQuit(message->Append<uint8_t>(0xcc));
+
+    SuccessOrQuit(Tlv::FindTlvValueOffset(*message, /* aType */ 6, valueOffset, length) != kErrorNone);
+    VerifyOrQuit(valueOffset == offset + sizeof(ExtendedTlv));
+    VerifyOrQuit(length == 2);
+    SuccessOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 2));
+    VerifyOrQuit(buffer[0] == 0xbb);
+    VerifyOrQuit(buffer[1] == 0xcc);
+    VerifyOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 3) == kErrorParse);
+
+    // Add an extended TLV with overflow length.
+
+    offset = message->GetLength();
+
+    extTlv.SetType(7);
+    extTlv.SetLength(0xffff);
+    SuccessOrQuit(message->Append(extTlv));
+    SuccessOrQuit(message->Append<uint8_t>(0x11));
+
+    VerifyOrQuit(Tlv::FindTlvValueOffset(*message, /* aType */ 7, valueOffset, length) != kErrorNone);
+    VerifyOrQuit(Tlv::ReadTlvValue(*message, offset, buffer, 1) == kErrorParse);
+
+    message->Free();
+
+    testFreeInstance(instance);
+}
+
+} // namespace ot
+
+int main(void)
+{
+    ot::TestTlv();
+    printf("All tests passed\n");
+    return 0;
+}
diff --git a/tests/unit/test_toolchain.cpp b/tests/unit/test_toolchain.cpp
index 22993e8..643b502 100644
--- a/tests/unit/test_toolchain.cpp
+++ b/tests/unit/test_toolchain.cpp
@@ -109,10 +109,7 @@
     VerifyOrQuit(sizeof(otNetifAddress) == otNetifAddress_Size_c(), "otNetifAddress should the same in C & C++");
 }
 
-void test_addr_bitfield(void)
-{
-    VerifyOrQuit(CreateNetif_c().mScopeOverrideValid == true, "test_addr_size_cpp");
-}
+void test_addr_bitfield(void) { VerifyOrQuit(CreateNetif_c().mScopeOverrideValid == true, "test_addr_size_cpp"); }
 
 void test_packed_alignment(void)
 {
diff --git a/tests/unit/test_toolchain_c.c b/tests/unit/test_toolchain_c.c
index 0159e42..cf34464 100644
--- a/tests/unit/test_toolchain_c.c
+++ b/tests/unit/test_toolchain_c.c
@@ -36,15 +36,9 @@
 
 #include "test_util.h"
 
-uint32_t otNetifAddress_Size_c()
-{
-    return sizeof(otNetifAddress);
-}
+uint32_t otNetifAddress_Size_c() { return sizeof(otNetifAddress); }
 
-uint32_t otNetifAddress_offset_mNext_c()
-{
-    return offsetof(otNetifAddress, mNext);
-}
+uint32_t otNetifAddress_offset_mNext_c() { return offsetof(otNetifAddress, mNext); }
 
 otNetifAddress CreateNetif_c()
 {
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/third_party/mbedtls/mbedtls-config.h b/third_party/mbedtls/mbedtls-config.h
index 997870c..ffb50df 100644
--- a/third_party/mbedtls/mbedtls-config.h
+++ b/third_party/mbedtls/mbedtls-config.h
@@ -86,6 +86,9 @@
 
 #if OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE
 #define MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
+#endif
+
+#if OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE || OPENTHREAD_CONFIG_TLS_ENABLE
 #define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
 #endif
 
@@ -103,7 +106,9 @@
 #define MBEDTLS_BASE64_C
 #define MBEDTLS_ECDH_C
 #define MBEDTLS_ECDSA_C
+#if OPENTHREAD_CONFIG_DETERMINISTIC_ECDSA_ENABLE
 #define MBEDTLS_ECDSA_DETERMINISTIC
+#endif
 #define MBEDTLS_OID_C
 #define MBEDTLS_PEM_PARSE_C
 #define MBEDTLS_PK_WRITE_C
diff --git a/third_party/openthread-test-driver/LICENSE b/third_party/openthread-test-driver/LICENSE
deleted file mode 100644
index d511905..0000000
--- a/third_party/openthread-test-driver/LICENSE
+++ /dev/null
@@ -1,339 +0,0 @@
-		    GNU GENERAL PUBLIC LICENSE
-		       Version 2, June 1991
-
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-			    Preamble
-
-  The licenses for most software are designed to take away your
-freedom to share and change it.  By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users.  This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it.  (Some other Free Software Foundation software is covered by
-the GNU Lesser General Public License instead.)  You can apply it to
-your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
-  To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have.  You must make sure that they, too, receive or can get the
-source code.  And you must show them these terms so they know their
-rights.
-
-  We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
-  Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software.  If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
-  Finally, any free program is threatened constantly by software
-patents.  We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary.  To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-		    GNU GENERAL PUBLIC LICENSE
-   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-  0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License.  The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language.  (Hereinafter, translation is included without limitation in
-the term "modification".)  Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope.  The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
-  1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
-  2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
-    a) You must cause the modified files to carry prominent notices
-    stating that you changed the files and the date of any change.
-
-    b) You must cause any work that you distribute or publish, that in
-    whole or in part contains or is derived from the Program or any
-    part thereof, to be licensed as a whole at no charge to all third
-    parties under the terms of this License.
-
-    c) If the modified program normally reads commands interactively
-    when run, you must cause it, when started running for such
-    interactive use in the most ordinary way, to print or display an
-    announcement including an appropriate copyright notice and a
-    notice that there is no warranty (or else, saying that you provide
-    a warranty) and that users may redistribute the program under
-    these conditions, and telling the user how to view a copy of this
-    License.  (Exception: if the Program itself is interactive but
-    does not normally print such an announcement, your work based on
-    the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole.  If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works.  But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
-  3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
-    a) Accompany it with the complete corresponding machine-readable
-    source code, which must be distributed under the terms of Sections
-    1 and 2 above on a medium customarily used for software interchange; or,
-
-    b) Accompany it with a written offer, valid for at least three
-    years, to give any third party, for a charge no more than your
-    cost of physically performing source distribution, a complete
-    machine-readable copy of the corresponding source code, to be
-    distributed under the terms of Sections 1 and 2 above on a medium
-    customarily used for software interchange; or,
-
-    c) Accompany it with the information you received as to the offer
-    to distribute corresponding source code.  (This alternative is
-    allowed only for noncommercial distribution and only if you
-    received the program in object code or executable form with such
-    an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it.  For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable.  However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
-  4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License.  Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
-  5. You are not required to accept this License, since you have not
-signed it.  However, nothing else grants you permission to modify or
-distribute the Program or its derivative works.  These actions are
-prohibited by law if you do not accept this License.  Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
-  6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions.  You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
-  7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all.  For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices.  Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
-  8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded.  In such case, this License incorporates
-the limitation as if written in the body of this License.
-
-  9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number.  If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation.  If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
-  10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission.  For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this.  Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
-			    NO WARRANTY
-
-  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
-  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
-		     END OF TERMS AND CONDITIONS
-
-	    How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
-    Gnomovision version 69, Copyright (C) year name of author
-    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary.  Here is a sample; alter the names:
-
-  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
-  `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
-  <signature of Ty Coon>, 1 April 1989
-  Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs.  If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.
diff --git a/third_party/openthread-test-driver/README.md b/third_party/openthread-test-driver/README.md
deleted file mode 100644
index acffbab..0000000
--- a/third_party/openthread-test-driver/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Automake test-driver
-
-## URL
-
-http://git.savannah.gnu.org/cgit/automake.git/tree/lib/test-driver
-
-## Version
-
-2016-01-11.22
-
-## License
-
-GPLv2
-
-## License File
-
-[LICENSE](https://www.gnu.org/licenses/)
-
-## Description
-
-Automake testsuite driver script.
-
-## Modifications
-
-Change the test-driver automake script to support
-parallel test scripts execution, each one initiated
-with a unique number.
diff --git a/third_party/openthread-test-driver/test-driver b/third_party/openthread-test-driver/test-driver
deleted file mode 100755
index 96f6db8..0000000
--- a/third_party/openthread-test-driver/test-driver
+++ /dev/null
@@ -1,165 +0,0 @@
-#! /bin/sh
-# test-driver - basic testsuite driver script.
-
-scriptversion=2016-01-11.22; # UTC
-
-# Copyright (C) 2011-2015 Free Software Foundation, Inc.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2, or (at your option)
-# any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-# As a special exception to the GNU General Public License, if you
-# distribute this file as part of a program that contains a
-# configuration script generated by Autoconf, you may include it under
-# the same distribution terms that you use for the rest of that program.
-
-# This file is maintained in Automake, please report
-# bugs to <bug-automake@gnu.org> or send patches to
-# <automake-patches@gnu.org>.
-
-# Make unconditional expansion of undefined variables an error.  This
-# helps a lot in preventing typo-related bugs.
-set -u
-
-usage_error ()
-{
-  echo "$0: $*" >&2
-  print_usage >&2
-  exit 2
-}
-
-print_usage ()
-{
-  cat <<END
-Usage:
-  test-driver --test-name=NAME --log-file=PATH --trs-file=PATH
-              [--expect-failure={yes|no}] [--color-tests={yes|no}]
-              [--enable-hard-errors={yes|no}] [--]
-              TEST-SCRIPT [TEST-SCRIPT-ARGUMENTS]
-The '--test-name', '--log-file' and '--trs-file' options are mandatory.
-END
-}
-
-test_name= # Used for reporting.
-log_file=  # Where to save the output of the test script.
-trs_file=  # Where to save the metadata of the test run.
-expect_failure=no
-color_tests=no
-enable_hard_errors=yes
-while test $# -gt 0; do
-  case $1 in
-  --help) print_usage; exit $?;;
-  --version) echo "test-driver $scriptversion"; exit $?;;
-  --test-name) test_name=$2; shift;;
-  --log-file) log_file=$2; shift;;
-  --trs-file) trs_file=$2; shift;;
-  --color-tests) color_tests=$2; shift;;
-  --expect-failure) expect_failure=$2; shift;;
-  --enable-hard-errors) enable_hard_errors=$2; shift;;
-  --) shift; break;;
-  -*) usage_error "invalid option: '$1'";;
-   *) break;;
-  esac
-  shift
-done
-
-missing_opts=
-test x"$test_name" = x && missing_opts="$missing_opts --test-name"
-test x"$log_file"  = x && missing_opts="$missing_opts --log-file"
-test x"$trs_file"  = x && missing_opts="$missing_opts --trs-file"
-if test x"$missing_opts" != x; then
-  usage_error "the following mandatory options are missing:$missing_opts"
-fi
-
-if test $# -eq 0; then
-  usage_error "missing argument"
-fi
-
-if test $color_tests = yes; then
-  # Keep this in sync with 'lib/am/check.am:$(am__tty_colors)'.
-  red='' # Red.
-  grn='' # Green.
-  lgn='' # Light green.
-  blu='' # Blue.
-  mgn='' # Magenta.
-  std=''     # No color.
-else
-  red= grn= lgn= blu= mgn= std=
-fi
-
-do_exit='rm -f $log_file $trs_file; (exit $st); exit $st'
-trap "st=129; $do_exit" 1
-trap "st=130; $do_exit" 2
-trap "st=141; $do_exit" 13
-trap "st=143; $do_exit" 15
-
-# Test script is run here.
-# Get an unique offset
-lock_path="/tmp/offset"
-OFFSET=0
-while true; do
-    mkdir "${lock_path}.${OFFSET}.lock.d" > /dev/null 2>&1
-    if [ $? -eq 0 ]; then
-        break
-    fi
-    OFFSET=$(expr $OFFSET + 1)
-done
-
-# Run a test
-TEST_NAME="${test_name}" PORT_OFFSET=$OFFSET python3 "$@" >$log_file 2>&1
-estatus=$?
-
-# Return the offset
-rm -rf "${lock_path}.${OFFSET}.lock.d"
-# Remove the flash files
-rm -f tmp/${OFFSET}_*.flash tmp/${OFFSET}_*.data tmp/${OFFSET}_*.swap
-
-if test $enable_hard_errors = no && test $estatus -eq 99; then
-  tweaked_estatus=1
-else
-  tweaked_estatus=$estatus
-fi
-
-case $tweaked_estatus:$expect_failure in
-  0:yes) col=$red res=XPASS recheck=yes gcopy=yes;;
-  0:*)   col=$grn res=PASS  recheck=no  gcopy=no;;
-  77:*)  col=$blu res=SKIP  recheck=no  gcopy=yes;;
-  99:*)  col=$mgn res=ERROR recheck=yes gcopy=yes;;
-  *:yes) col=$lgn res=XFAIL recheck=no  gcopy=yes;;
-  *:*)   col=$red res=FAIL  recheck=yes gcopy=yes;;
-esac
-
-# Report the test outcome and exit status in the logs, so that one can
-# know whether the test passed or failed simply by looking at the '.log'
-# file, without the need of also peaking into the corresponding '.trs'
-# file (automake bug#11814).
-echo "$res $test_name (exit status: $estatus)" >>$log_file
-
-# Report outcome to console.
-echo "${col}${res}${std}: $test_name"
-
-# Register the test result, and other relevant metadata.
-echo ":test-result: $res" > $trs_file
-echo ":global-test-result: $res" >> $trs_file
-echo ":recheck: $recheck" >> $trs_file
-echo ":copy-in-global-log: $gcopy" >> $trs_file
-
-# Local Variables:
-# mode: shell-script
-# sh-indentation: 2
-# eval: (add-hook 'write-file-hooks 'time-stamp)
-# time-stamp-start: "scriptversion="
-# time-stamp-format: "%:y-%02m-%02d.%02H"
-# time-stamp-time-zone: "UTC0"
-# time-stamp-end: "; # UTC"
-# End:
diff --git a/third_party/tcplp/CMakeLists.txt b/third_party/tcplp/CMakeLists.txt
index 3ea41b6..9ec23b8 100644
--- a/third_party/tcplp/CMakeLists.txt
+++ b/third_party/tcplp/CMakeLists.txt
@@ -43,7 +43,6 @@
     lib/lbuf.c
 )
 
-set(tcplp_static_target "tcplp")
 
 string(REPLACE "-Wsign-compare" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
 
@@ -51,40 +50,58 @@
 
 string(REPLACE "-Wunused-parameter" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
 
-add_library(${tcplp_static_target} STATIC ${src_tcplp})
-target_compile_options(${tcplp_static_target}
-    PRIVATE
-        "-Wno-sign-compare"
-        "-Wno-unused-parameter"
-)
-set_target_properties(${tcplp_static_target} PROPERTIES OUTPUT_NAME tcplp)
-target_include_directories(${tcplp_static_target}
-    PUBLIC
-        ${CMAKE_CURRENT_SOURCE_DIR}/bsdtcp
-        ${CMAKE_CURRENT_SOURCE_DIR}/lib
-    PRIVATE
-        ${OT_PUBLIC_INCLUDES}
-)
-
-target_link_libraries(${tcplp_static_target}
-    PRIVATE
-        ot-config
-)
-
-# TCPlp calls functions that are defined by the core OpenThread (like
-# "otMessageWrite()"), so we need to add the core library (FTD or MTD, as
-# appropriate) as a link dependency.
-
 if(OT_FTD)
-    target_link_libraries(${tcplp_static_target}
+    add_library(tcplp-ftd STATIC ${src_tcplp})
+    target_compile_options(tcplp-ftd
+        PRIVATE
+            "-Wno-sign-compare"
+            "-Wno-unused-parameter"
+    )
+    set_target_properties(tcplp-ftd PROPERTIES OUTPUT_NAME tcplp-ftd)
+    target_include_directories(tcplp-ftd
+        PUBLIC
+            ${CMAKE_CURRENT_SOURCE_DIR}/bsdtcp
+            ${CMAKE_CURRENT_SOURCE_DIR}/lib
+        PRIVATE
+            ${OT_PUBLIC_INCLUDES}
+    )
+
+    target_link_libraries(tcplp-ftd
+        PRIVATE
+            ot-config
+    )
+
+    target_link_libraries(tcplp-ftd
         PRIVATE
             openthread-ftd
     )
+
 endif()
 
 if(OT_MTD)
-    target_link_libraries(${tcplp_static_target}
+    add_library(tcplp-mtd STATIC ${src_tcplp})
+    target_compile_options(tcplp-mtd
+        PRIVATE
+            "-Wno-sign-compare"
+            "-Wno-unused-parameter"
+    )
+    set_target_properties(tcplp-mtd PROPERTIES OUTPUT_NAME tcplp-mtd)
+    target_include_directories(tcplp-mtd
+        PUBLIC
+            ${CMAKE_CURRENT_SOURCE_DIR}/bsdtcp
+            ${CMAKE_CURRENT_SOURCE_DIR}/lib
+        PRIVATE
+            ${OT_PUBLIC_INCLUDES}
+    )
+
+    target_link_libraries(tcplp-mtd
+        PRIVATE
+            ot-config
+    )
+
+    target_link_libraries(tcplp-mtd
         PRIVATE
             openthread-mtd
     )
+
 endif()
diff --git a/third_party/tcplp/bsdtcp/tcp_input.c b/third_party/tcplp/bsdtcp/tcp_input.c
index 8211f2f..d79f2d2 100644
--- a/third_party/tcplp/bsdtcp/tcp_input.c
+++ b/third_party/tcplp/bsdtcp/tcp_input.c
@@ -241,8 +241,12 @@
 		tp->t_dupacks = 0;
 		tp->t_bytes_acked = 0;
 		EXIT_RECOVERY(tp->t_flags);
+		/*
+		 * samkumar: I added the cast to uint64_t below to fix an OpenThread
+		 * code scanning alert relating to integer overflow in multiplication.
+		 */
 		tp->snd_ssthresh = max(2, min(tp->snd_wnd, tp->snd_cwnd) / 2 /
-		    tp->t_maxseg) * tp->t_maxseg;
+		    tp->t_maxseg) * ((uint64_t) tp->t_maxseg);
 		tp->snd_cwnd = tp->t_maxseg;
 
 		/*
@@ -766,13 +770,14 @@
 		 */
 
 		tcp_dooptions(&to, optp, optlen, TO_SYN);
-		tp = tcplp_sys_accept_ready(tpl, &ip6->ip6_dst, th->th_sport); // Try to allocate an active socket to accept into
+		tp = tcplp_sys_accept_ready(tpl, &ip6->ip6_src, th->th_sport); // Try to allocate an active socket to accept into
 		if (tp == NULL) {
 			/* If we couldn't allocate, just ignore the SYN. */
 			return IPPROTO_DONE;
 		}
 		if (tp == (struct tcpcb *) -1) {
 			rstreason = ECONNREFUSED;
+			tp = NULL;
 			goto dropwithreset;
 		}
 		tcp_state_change(tp, TCPS_SYN_RECEIVED);
@@ -2012,9 +2017,14 @@
 					tp->snd_nxt = th->th_ack;
 					tp->snd_cwnd = tp->t_maxseg;
 					(void) tcp_output(tp);
+					/*
+					 * samkumar: I added casts to uint64_t below to
+					 * fix an OpenThread code scanning alert relating
+					 * to integer overflow in multiplication.
+					 */
 					tp->snd_cwnd = tp->snd_ssthresh +
-					     tp->t_maxseg *
-					     (tp->t_dupacks - tp->snd_limited);
+					     ((uint64_t) tp->t_maxseg) *
+					     ((uint64_t) (tp->t_dupacks - tp->snd_limited));
 #ifdef INSTRUMENT_TCP
 					tcplp_sys_log("TCP SET_cwnd %d", (int) tp->snd_cwnd);
 #endif
diff --git a/third_party/tcplp/lib/bitmap.c b/third_party/tcplp/lib/bitmap.c
index 70d03c1..a0dca48 100644
--- a/third_party/tcplp/lib/bitmap.c
+++ b/third_party/tcplp/lib/bitmap.c
@@ -67,7 +67,9 @@
     } else {
         *first_byte_set |= first_byte_mask;
         memset(first_byte_set + 1, 0xFF, (size_t) (last_byte_set - first_byte_set - 1));
-        *last_byte_set |= last_byte_mask;
+        if (last_byte_mask != 0x00) {
+            *last_byte_set |= last_byte_mask;
+        }
     }
 }
 
@@ -89,7 +91,9 @@
     } else {
         *first_byte_clear &= first_byte_mask;
         memset(first_byte_clear + 1, 0x00, (size_t) (last_byte_clear - first_byte_clear - 1));
-        *last_byte_clear &= last_byte_mask;
+        if (last_byte_mask != 0xFF) {
+            *last_byte_clear &= last_byte_mask;
+        }
     }
 }
 
@@ -131,6 +135,67 @@
     return numset;
 }
 
+static inline uint8_t bmp_read_bit(uint8_t* buf, size_t i) {
+    size_t byte_index = i >> 3;
+    size_t bit_index = i & 0x7; // Amount to left shift to get bit in MSB
+    return ((uint8_t) (buf[byte_index] << bit_index)) >> 7;
+}
+
+static inline void bmp_write_bit(uint8_t* buf, size_t i, uint8_t bit) {
+    size_t byte_index = i >> 3;
+    size_t bit_index = i & 0x7; // Amount to left shift to get bit in MSB
+    size_t bit_shift = 7 - bit_index; // Amount to right shift to get bit in LSB
+    buf[byte_index] = (buf[byte_index] & ~(1 << bit_shift)) | (bit << bit_shift);
+}
+
+static inline uint8_t bmp_read_byte(uint8_t* buf, size_t i) {
+    size_t byte_index = i >> 3;
+    size_t bit_index = i & 0x7; // Amount to left shift to get bit in MSB
+    if (bit_index == 0) {
+        return buf[byte_index];
+    }
+    return (buf[byte_index] << bit_index) | (buf[byte_index + 1] >> (8 - bit_index));
+}
+
+static inline void bmp_write_byte(uint8_t* buf, size_t i, uint8_t byte) {
+    size_t byte_index = i >> 3;
+    size_t bit_index = i & 0x7; // Amount to left shift to get bit in MSB
+    if (bit_index == 0) {
+        buf[byte_index] = byte;
+        return;
+    }
+    buf[byte_index] = (buf[byte_index] & (0xFF << (8 - bit_index))) | (byte >> bit_index);
+    buf[byte_index + 1] = (buf[byte_index + 1] & (0xFF >> bit_index)) | (byte << (8 - bit_index));
+}
+
+void bmp_swap(uint8_t* buf, size_t start_1, size_t start_2, size_t len) {
+    while ((len & 0x7) != 0) {
+        uint8_t bit_1 = bmp_read_bit(buf, start_1);
+        uint8_t bit_2 = bmp_read_bit(buf, start_2);
+        if (bit_1 != bit_2) {
+            bmp_write_bit(buf, start_1, bit_2);
+            bmp_write_bit(buf, start_2, bit_1);
+        }
+
+        start_1++;
+        start_2++;
+        len--;
+    }
+
+    while (len != 0) {
+        uint8_t byte_1 = bmp_read_byte(buf, start_1);
+        uint8_t byte_2 = bmp_read_byte(buf, start_2);
+        if (byte_1 != byte_2) {
+            bmp_write_byte(buf, start_1, byte_2);
+            bmp_write_byte(buf, start_2, byte_1);
+        }
+
+        start_1 += 8;
+        start_2 += 8;
+        len -= 8;
+    }
+}
+
 int bmp_isempty(uint8_t* buf, size_t buflen) {
     uint8_t* bufend = buf + buflen;
     while (buf < bufend) {
diff --git a/third_party/tcplp/lib/bitmap.h b/third_party/tcplp/lib/bitmap.h
index a4ef41a..9193ce8 100644
--- a/third_party/tcplp/lib/bitmap.h
+++ b/third_party/tcplp/lib/bitmap.h
@@ -57,6 +57,11 @@
    which case it returns exactly the number of set bits it found. */
 size_t bmp_countset(uint8_t* buf, size_t buflen, size_t start, size_t limit);
 
+/* Swaps two non-overlapping regions of the bitmap. START_1 is the index of
+   the first region, START_2 is the index of the secoind region, and LEN is
+   the length of each region, in bits. */
+void bmp_swap(uint8_t* buf, size_t start_1, size_t start_2, size_t len);
+
 /* Returns 1 if the bitmap is all zeros, and 0 otherwise. */
 int bmp_isempty(uint8_t* buf, size_t buflen);
 
diff --git a/third_party/tcplp/lib/cbuf.c b/third_party/tcplp/lib/cbuf.c
index b7793ac..5662ef2 100644
--- a/third_party/tcplp/lib/cbuf.c
+++ b/third_party/tcplp/lib/cbuf.c
@@ -73,67 +73,73 @@
 
 void cbuf_init(struct cbufhead* chdr, uint8_t* buf, size_t len) {
     chdr->r_index = 0;
-    chdr->w_index = 0;
+    chdr->used = 0;
     chdr->size = len;
     chdr->buf = buf;
 }
 
 size_t cbuf_used_space(struct cbufhead* chdr) {
-    if (chdr->w_index >= chdr->r_index) {
-        return chdr->w_index - chdr->r_index;
-    } else {
-        return chdr->size + chdr->w_index - chdr->r_index;
-    }
+    return chdr->used;
 }
 
-/* There's always one byte of lost space so I can distinguish between a full
-   buffer and an empty buffer. */
 size_t cbuf_free_space(struct cbufhead* chdr) {
-    return chdr->size - 1 - cbuf_used_space(chdr);
+    return chdr->size - chdr->used;
 }
 
 size_t cbuf_size(struct cbufhead* chdr) {
-    return chdr->size - 1;
+    return chdr->size;
 }
 
 bool cbuf_empty(struct cbufhead* chdr) {
-    return (chdr->w_index == chdr->r_index);
+    return chdr->used == 0;
+}
+
+static inline size_t cbuf_get_w_index(const struct cbufhead* chdr) {
+    size_t until_end = chdr->size - chdr->r_index;
+    if (chdr->used < until_end) {
+        return chdr->r_index + chdr->used;
+    } else {
+        return chdr->used - until_end;
+    }
 }
 
 size_t cbuf_write(struct cbufhead* chdr, const void* data, size_t data_offset, size_t data_len, cbuf_copier_t copy_from) {
     size_t free_space = cbuf_free_space(chdr);
     uint8_t* buf_data;
-    size_t fw_index;
+    size_t w_index;
     size_t bytes_to_end;
     if (free_space < data_len) {
         data_len = free_space;
     }
     buf_data = chdr->buf;
-    fw_index = (chdr->w_index + data_len) % chdr->size;
-    if (fw_index >= chdr->w_index) {
-        copy_from(buf_data, chdr->w_index, data, data_offset, data_len);
+    w_index = cbuf_get_w_index(chdr);
+    bytes_to_end = chdr->size - w_index;
+    if (data_len <= bytes_to_end) {
+        copy_from(buf_data, w_index, data, data_offset, data_len);
     } else {
-        bytes_to_end = chdr->size - chdr->w_index;
-        copy_from(buf_data, chdr->w_index, data, data_offset, bytes_to_end);
+        copy_from(buf_data, w_index, data, data_offset, bytes_to_end);
         copy_from(buf_data, 0, data, data_offset + bytes_to_end, data_len - bytes_to_end);
     }
-    chdr->w_index = fw_index;
+    chdr->used += data_len;
     return data_len;
 }
 
 void cbuf_read_unsafe(struct cbufhead* chdr, void* data, size_t data_offset, size_t numbytes, int pop, cbuf_copier_t copy_into) {
     uint8_t* buf_data = chdr->buf;
-    size_t fr_index = (chdr->r_index + numbytes) % chdr->size;
-    size_t bytes_to_end;
-    if (fr_index >= chdr->r_index) {
+    size_t bytes_to_end = chdr->size - chdr->r_index;
+    if (numbytes < bytes_to_end) {
         copy_into(data, data_offset, buf_data, chdr->r_index, numbytes);
+        if (pop) {
+            chdr->r_index += numbytes;
+            chdr->used -= numbytes;
+        }
     } else {
-        bytes_to_end = chdr->size - chdr->r_index;
         copy_into(data, data_offset, buf_data, chdr->r_index, bytes_to_end);
         copy_into(data, data_offset + bytes_to_end, buf_data, 0, numbytes - bytes_to_end);
-    }
-    if (pop) {
-        chdr->r_index = fr_index;
+        if (pop) {
+            chdr->r_index = numbytes - bytes_to_end;
+            chdr->used -= numbytes;
+        }
     }
 }
 
@@ -167,22 +173,113 @@
         numbytes = used_space;
     }
     chdr->r_index = (chdr->r_index + numbytes) % chdr->size;
+    chdr->used -= numbytes;
     return numbytes;
 }
 
+static void cbuf_swap(struct cbufhead* chdr, uint8_t* bitmap, size_t start_1, size_t start_2, size_t length) {
+    size_t i;
+
+    /* Swap the data regions. */
+    for (i = 0; i != length; i++) {
+        uint8_t temp = chdr->buf[start_1 + i];
+        chdr->buf[start_1 + i] = chdr->buf[start_2 + i];
+        chdr->buf[start_2 + i] = temp;
+    }
+
+    /* Swap the bitmaps. */
+    if (bitmap) {
+        bmp_swap(bitmap, start_1, start_2, length);
+    }
+}
+
+void cbuf_contiguify(struct cbufhead* chdr, uint8_t* bitmap) {
+    /*
+     * We treat contiguify as a special case of rotation. In principle, we
+     * could make this more efficient by inspecting R_INDEX, W_INDEX, and the
+     * bitmap to only move around in-sequence data and buffered out-of-sequence
+     * data, while ignoring the other bytes in the circular buffer. We leave
+     * this as an optimization to implement if/when it becomes necessary.
+     *
+     * The rotation algorithm is recursive. It is parameterized by three
+     * arguments. START_IDX is the index of the first element of the subarray
+     * that is being rotated. END_IDX is one plus the index of the last element
+     * of the subarray that is being rotated. MOVE_TO_START_IDX is the index of
+     * the element that should be located at START_IDX after the rotation.
+     *
+     * The algorithm is as follows. First, identify the largest block of data
+     * starting at MOVE_TO_START_IDX that can be swapped with data starting at
+     * START_IDX. If MOVE_TO_START_IDX is right at the midpoint of the array,
+     * then we're done. If it isn't, then we can treat the block of data that
+     * was just swapped to the beginning of the array as "done", and then
+     * complete the rotation by recursively rotating the rest of the array.
+     *
+     * Here's an example. Suppose that the array is "1 2 3 4 5 6 7 8 9" and
+     * MOVE_TO_START_IDX is the index of the element "3". First, we swap "1 2"
+     * AND "3 4" to get "3 4 1 2 5 6 7 8 9". Then, we recursively rotate the
+     * subarray "1 2 5 6 7 8 9", with MOVE_TO_START_IDX being the index of the
+     * element "5". The final array is "3 4 5 6 7 8 9 1 2".
+     *
+     * Here's another example. Suppose that the array is "1 2 3 4 5 6 7 8 9"
+     * and MOVE_TO_START_IDX is the index of the element "6". First, we swap
+     * "1 2 3 4" and "6 7 8 9" to get "6 7 8 9 5 1 2 3 4". Then, we recursively
+     * rotate the subarray "5 1 2 3 4", with MOVE_TO_START_IDX being the index
+     * of the element "1". The final array is "6 7 8 9 1 2 3 4 5".
+     *
+     * In order for this to work, it's important that the blocks that we
+     * choose are maximally large. If, in the first example, we swap only the
+     * elements "1" and "3", then the algorithm won't work. Note that "1 2" and
+     * "3 4" corresponds to maximally large blocks because if we make the
+     * blocks any bigger, they would overlap (e.g., "1 2 3" and "3 4 5"). In
+     * the second example, the block "6 7 8 9" is maximally large because we
+     * reach the end of the subarray.
+     *
+     * The algorithm above is tail-recursive (i.e., there's no more work to do
+     * after recursively rotating the subarray), so we write it as a while
+     * loop below. Each iteration of the while loop identifies the blocks to
+     * swap, swaps the blocks, and then sets up the indices such that the
+     * next iteration of the loop rotates the appropriate subarray.
+     *
+     * The performance of the algorithm is linear in the length of the array,
+     * with constant space overhead.
+     */
+    size_t start_idx = 0;
+    const size_t end_idx = chdr->size;
+    size_t move_to_start_idx = chdr->r_index;
+
+    /* Invariant: start_idx <= move_to_start_idx <= end_idx */
+    while (start_idx < move_to_start_idx && move_to_start_idx < end_idx) {
+        size_t distance_from_start = move_to_start_idx - start_idx;
+        size_t distance_to_end = end_idx - move_to_start_idx;
+        if (distance_from_start <= distance_to_end) {
+            cbuf_swap(chdr, bitmap, start_idx, move_to_start_idx, distance_from_start);
+            start_idx = move_to_start_idx;
+            move_to_start_idx = move_to_start_idx + distance_from_start;
+        } else {
+            cbuf_swap(chdr, bitmap, start_idx, move_to_start_idx, distance_to_end);
+            start_idx = start_idx + distance_to_end;
+            // move_to_start_idx does not change
+        }
+    }
+
+    /* Finally, fix up the indices. */
+    chdr->r_index = 0;
+}
+
 void cbuf_reference(const struct cbufhead* chdr, otLinkedBuffer* first, otLinkedBuffer* second) {
-    if (chdr->w_index >= chdr->r_index) {
+    size_t until_end = chdr->size - chdr->r_index;
+    if (chdr->used <= until_end) {
         first->mNext = NULL;
         first->mData = &chdr->buf[chdr->r_index];
-        first->mLength = (uint16_t) (chdr->w_index - chdr->r_index);
+        first->mLength = (uint16_t) chdr->used;
     } else {
         first->mNext = second;
         first->mData = &chdr->buf[chdr->r_index];
-        first->mLength = (uint16_t) (chdr->size - chdr->r_index);
+        first->mLength = (uint16_t) until_end;
 
         second->mNext = NULL;
         second->mData = &chdr->buf[0];
-        second->mLength = (uint16_t) chdr->w_index;
+        second->mLength = (uint16_t) (chdr->used - until_end);
     }
 }
 
@@ -197,7 +294,7 @@
     } else if (offset + numbytes > free_space) {
         numbytes = free_space - offset;
     }
-    start_index = (chdr->w_index + offset) % chdr->size;
+    start_index = (cbuf_get_w_index(chdr) + offset) % chdr->size;
     end_index = (start_index + numbytes) % chdr->size;
     if (end_index >= start_index) {
         copy_from(buf_data, start_index, data, data_offset, numbytes);
@@ -220,29 +317,29 @@
 }
 
 size_t cbuf_reass_merge(struct cbufhead* chdr, size_t numbytes, uint8_t* bitmap) {
-    size_t old_w = chdr->w_index;
+    size_t old_w = cbuf_get_w_index(chdr);
     size_t free_space = cbuf_free_space(chdr);
     size_t bytes_to_end;
     if (numbytes > free_space) {
         numbytes = free_space;
     }
-    chdr->w_index = (chdr->w_index + numbytes) % chdr->size;
     if (bitmap) {
-        if (chdr->w_index >= old_w) {
+        bytes_to_end = chdr->size - old_w;
+        if (numbytes <= bytes_to_end) {
             bmp_clrrange(bitmap, old_w, numbytes);
         } else {
-            bytes_to_end = chdr->size - old_w;
             bmp_clrrange(bitmap, old_w, bytes_to_end);
             bmp_clrrange(bitmap, 0, numbytes - bytes_to_end);
         }
     }
+    chdr->used += numbytes;
     return numbytes;
 }
 
 size_t cbuf_reass_count_set(struct cbufhead* chdr, size_t offset, uint8_t* bitmap, size_t limit) {
     size_t bitmap_size = BITS_TO_BYTES(chdr->size);
     size_t until_end;
-    offset = (chdr->w_index + offset) % chdr->size;
+    offset = (cbuf_get_w_index(chdr) + offset) % chdr->size;
     until_end = bmp_countset(bitmap, bitmap_size, offset, limit);
     if (until_end >= limit || until_end < (chdr->size - offset)) {
         // If we already hit the limit, or if the streak ended before wrapping, then stop here
@@ -254,7 +351,7 @@
 }
 
 int cbuf_reass_within_offset(struct cbufhead* chdr, size_t offset, size_t index) {
-    size_t range_start = chdr->w_index;
+    size_t range_start = cbuf_get_w_index(chdr);
     size_t range_end = (range_start + offset) % chdr->size;
     if (range_end >= range_start) {
         return index >= range_start && index < range_end;
diff --git a/third_party/tcplp/lib/cbuf.h b/third_party/tcplp/lib/cbuf.h
index 7ba6b6d..1668723 100644
--- a/third_party/tcplp/lib/cbuf.h
+++ b/third_party/tcplp/lib/cbuf.h
@@ -42,7 +42,7 @@
 /* Represents a circular buffer. */
 struct cbufhead {
     size_t r_index;
-    size_t w_index;
+    size_t used;
     size_t size;
     uint8_t* buf;
 };
@@ -64,7 +64,7 @@
 /* Writes data to the back of the circular buffer using the specified copier. */
 size_t cbuf_write(struct cbufhead* chdr, const void* data, size_t data_offset, size_t data_len, cbuf_copier_t copy_from);
 
-/* Reads data from the front ofthe circular buffer using the specified copier. */
+/* Reads data from the front of the circular buffer using the specified copier. */
 size_t cbuf_read(struct cbufhead* chdr, void* data, size_t data_offset, size_t numbytes, int pop, cbuf_copier_t copy_into);
 
 /* Reads data at the specified offset, in bytes, from the front of the circular buffer using the specified copier. */
@@ -85,12 +85,15 @@
 /* Returns true if the circular buffer is empty, and false if it is not empty. */
 bool cbuf_empty(struct cbufhead* chdr);
 
+/* Rotates the circular buffer's data so that the "used" portion begins at the beginning of the buffer. */
+void cbuf_contiguify(struct cbufhead* chdr, uint8_t* bitmap);
+
 /* Populates the provided otLinkedBuffers to reference the data currently in the circular buffer. */
 void cbuf_reference(const struct cbufhead* chdr, struct otLinkedBuffer* first, struct otLinkedBuffer* second);
 
 /* Writes DATA at the end of the circular buffer without making it available for
    reading. This data is said to be "out-of-sequence". OFFSET is position at
-   which to write these bytes, relative to the positoin where cbuf_write would
+   which to write these bytes, relative to the position where cbuf_write would
    write them. Each bit in the BITMAP corresponds to a byte in the circular
    buffer; the bits corresponding to the bytes containing the newly written
    data are set. The index of the first byte written is stored into FIRSTINDEX,
@@ -105,7 +108,15 @@
 size_t cbuf_reass_merge(struct cbufhead* chdr, size_t numbytes, uint8_t* bitmap);
 
 /* Counts the number of contiguous out-of-sequence bytes at the specified
-   OFFSET, until the count reaches the specified LIMIT. */
+   OFFSET, until the count reaches the specified LIMIT. Note that, for a given,
+   limit, this function might overcount the length of the continuous
+   out-of-sequence bytes and return a greater number; the caller is assumed to
+   handle this appropriately (i.e., treating the limit not as a hard upper
+   bound on the return value, but rather as, "I don't care if more bits than
+   this are set"). Just because the function returns something more than
+   LIMIT, it doesn't necessarily mean that more than LIMIT bits are actually
+   set. Note that LIMIT should never be set to a value greater than the number
+   of bytes in the circular buffer. */
 size_t cbuf_reass_count_set(struct cbufhead* chdr, size_t offset, uint8_t* bitmap, size_t limit);
 
 /* Returns a true value iff INDEX is the index of a byte within OFFSET bytes
diff --git a/third_party/tcplp/lib/lbuf.c b/third_party/tcplp/lib/lbuf.c
index 91dd456..ba39c5d 100644
--- a/third_party/tcplp/lib/lbuf.c
+++ b/third_party/tcplp/lib/lbuf.c
@@ -55,6 +55,7 @@
 
 void lbuf_extend(struct lbufhead* buffer, size_t numbytes) {
     buffer->tail->mLength += numbytes;
+    buffer->length += numbytes;
 }
 
 size_t lbuf_pop(struct lbufhead* buffer, size_t numbytes, uint32_t* ntraversed) {
diff --git a/third_party/tcplp/lib/test/Makefile b/third_party/tcplp/lib/test/Makefile
new file mode 100644
index 0000000..5613cbd
--- /dev/null
+++ b/third_party/tcplp/lib/test/Makefile
@@ -0,0 +1,16 @@
+CC=clang
+CFLAGS=-I ../../../../include -O2 -Wall
+
+all: test_all
+
+%.o: ../%.c
+	clang -c $(CFLAGS) $< -o $@
+
+test_all.o: test_all.c
+	clang -c $(CFLAGS) test_all.c -o $@
+
+test_all: test_all.o cbuf.o lbuf.o bitmap.o
+	clang test_all.o cbuf.o lbuf.o bitmap.o -o test_all
+
+clean:
+	rm -f *.o test_all
diff --git a/third_party/tcplp/lib/test/test_all.c b/third_party/tcplp/lib/test/test_all.c
new file mode 100644
index 0000000..9884259
--- /dev/null
+++ b/third_party/tcplp/lib/test/test_all.c
@@ -0,0 +1,219 @@
+#include <assert.h>
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <openthread/message.h>
+#include <openthread/tcp.h>
+
+#include "../bitmap.h"
+#include "../cbuf.h"
+
+uint32_t num_tests_passed = 0;
+uint32_t num_tests_failed = 0;
+
+uint16_t otMessageRead(const otMessage *aMessage, uint16_t aOffset, void *aBuf, uint16_t aLength) {
+    return aLength;
+}
+
+int otMessageWrite(otMessage *aMessage, uint16_t aOffset, const void *aBuf, uint16_t aLength) {
+    return aLength;
+}
+
+void bmp_print(uint8_t* buf, size_t buflen) {
+    size_t i;
+    for (i = 0; i < buflen; i++) {
+        printf("%02X", buf[i]);
+    }
+    printf("\n");
+}
+
+void bmp_test(const char* test_name, uint8_t* buf, size_t buflen, const char* contents) {
+    char buf_string[(buflen << 1) + 1];
+    buf_string[0] = '\0';
+    for (size_t i = 0; i < buflen; i++) {
+        snprintf(&buf_string[i << 1], 3, "%02X", buf[i]);
+    }
+    if (strcmp(contents, buf_string) == 0) {
+        printf("%s: PASS\n", test_name);
+        num_tests_passed++;
+    } else {
+        printf("%s: FAIL: %s vs. %s\n", test_name, contents, buf_string);
+        num_tests_failed++;
+    }
+}
+
+void test_bmp() {
+    size_t test_bmp_size = 8;
+    uint8_t buffer[test_bmp_size];
+
+    bmp_init(buffer, test_bmp_size);
+    bmp_test("bmp_init", buffer, test_bmp_size, "0000000000000000");
+
+    bmp_setrange(buffer, 11, 7);
+    bmp_test("bmp_setrange 1", buffer, test_bmp_size, "001FC00000000000");
+
+    bmp_setrange(buffer, 35, 3);
+    bmp_test("bmp_setrange 2", buffer, test_bmp_size, "001FC0001C000000");
+
+    bmp_setrange(buffer, 47, 4);
+    bmp_test("bmp_setrange 3", buffer, test_bmp_size, "001FC0001C01E000");
+
+    bmp_swap(buffer, 3, 36, 1);
+    bmp_test("bmp_swap 1", buffer, test_bmp_size, "101FC0001401E000");
+
+    bmp_swap(buffer, 0, 40, 24);
+    bmp_test("bmp_swap 2", buffer, test_bmp_size, "01E0000014101FC0");
+
+    bmp_swap(buffer, 2, 42, 15);
+    bmp_test("bmp_swap 3", buffer, test_bmp_size, "101F80001401E040");
+
+    bmp_swap(buffer, 13, 23, 2);
+    bmp_test("bmp_swap 4", buffer, test_bmp_size, "101981801401E040");
+
+    bmp_swap(buffer, 0, 35, 24);
+    bmp_test("bmp_swap 5", buffer, test_bmp_size, "A00F028002033020");
+}
+
+void cbuf_test(const char* test_name, struct cbufhead* chdr, const char* contents) {
+    char buf_string[chdr->size + 1];
+    struct otLinkedBuffer first;
+    struct otLinkedBuffer second;
+    cbuf_reference(chdr, &first, &second);
+
+
+    memcpy(&buf_string[0], &first.mData[0], first.mLength);
+    if (first.mNext != NULL) {
+        assert(first.mNext == &second);
+        memcpy(&buf_string[first.mLength], &second.mData[0], second.mLength);
+        assert(second.mNext == NULL);
+        buf_string[first.mLength + second.mLength] = '\0';
+    } else {
+        buf_string[first.mLength] = '\0';
+    }
+
+    if (strcmp(contents, buf_string) == 0) {
+        printf("%s: PASS\n", test_name);
+        num_tests_passed++;
+    } else {
+        printf("%s: FAIL: %s (%zu) vs. %s (%zu)\n", test_name, contents, strlen(contents), buf_string, strlen(buf_string));
+        num_tests_failed++;
+    }
+}
+
+void cbuf_write_string(struct cbufhead* chdr, const char* string) {
+    cbuf_write(chdr, string, 0, strlen(string), cbuf_copy_into_buffer);
+}
+
+void test_cbuf() {
+    uint8_t buffer[65];
+    uint8_t bitmap[8];
+    struct cbufhead chdr;
+
+    cbuf_init(&chdr, buffer, 64); // capacity is actually 64
+    cbuf_test("cbuf_init", &chdr, "");
+
+    cbuf_write_string(&chdr, "abcdefghijklmnopqrstuvwxyz0123456789");
+    cbuf_test("cbuf_write", &chdr, "abcdefghijklmnopqrstuvwxyz0123456789");
+
+    cbuf_pop(&chdr, 1);
+    cbuf_test("cbuf_pop", &chdr, "bcdefghijklmnopqrstuvwxyz0123456789");
+
+    cbuf_pop(&chdr, 5);
+    cbuf_test("cbuf_pop", &chdr, "ghijklmnopqrstuvwxyz0123456789");
+
+    cbuf_write_string(&chdr, "abcdefghijklmnopqrstuvwxyz01234567");
+    cbuf_test("cbuf_write", &chdr, "ghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01234567");
+
+    cbuf_contiguify(&chdr, NULL);
+    cbuf_test("cbuf_contiguify", &chdr, "ghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01234567");
+
+    cbuf_pop(&chdr, 50);
+    cbuf_test("cbuf_pop", &chdr, "uvwxyz01234567");
+
+    cbuf_write_string(&chdr, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); // yz overflows and isn't written
+    cbuf_test("cbuf_write", &chdr, "uvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx");
+
+    cbuf_contiguify(&chdr, NULL);
+    cbuf_test("cbuf_contiguify", &chdr, "uvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx");
+
+    cbuf_contiguify(&chdr, NULL); // check that a second "contiguify" operation doesn't mess things up
+    cbuf_test("cbuf_contiguify", &chdr, "uvwxyz01234567ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx");
+
+    cbuf_pop(&chdr, 20);
+    cbuf_test("cbuf_pop", &chdr, "GHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx");
+
+    cbuf_write_string(&chdr, "yz");
+    cbuf_test("cbuf_write", &chdr, "GHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
+
+    bmp_init(bitmap, 8);
+    bmp_test("bmp_init", bitmap, 8, "0000000000000000");
+
+    cbuf_reass_write(&chdr, 4, "@@@@@@@@@@@", 0, 11, bitmap, NULL, cbuf_copy_from_buffer);
+    cbuf_test("cbuf_reass_write (cbuf)", &chdr, "GHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
+    bmp_test("cbuf_reass_write (bitmap)", bitmap, 8, "03FF800000000000");
+
+    cbuf_contiguify(&chdr, bitmap);
+    cbuf_test("cbuf_contiguify (cbuf)", &chdr, "GHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
+    bmp_test("cbuf_reass_write (bitmap)", bitmap, 8, "0000000000003FF8");
+
+    cbuf_write_string(&chdr, "1234");
+    cbuf_test("cbuf_write", &chdr, "GHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234");
+
+    cbuf_reass_merge(&chdr, 9, bitmap);
+    cbuf_test("cbuf_reass_merge (cbuf)", &chdr, "GHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234@@@@@@@@@");
+    bmp_test("cbuf_reass_merge (bitmap)", bitmap, 8, "0000000000000018");
+
+    cbuf_reass_merge(&chdr, 2, bitmap);
+    cbuf_test("cbuf_reass_merge (cbuf)", &chdr, "GHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234@@@@@@@@@@@");
+    bmp_test("cbuf_reass_merge (bitmap)", bitmap, 8, "0000000000000000");
+
+    cbuf_pop(&chdr, 61);
+    cbuf_test("cbuf_pop", &chdr, "");
+}
+
+void test_cbuf_2() {
+    uint8_t buffer[32];
+    uint8_t bitmap[4];
+    struct cbufhead chdr;
+
+    cbuf_init(&chdr, buffer, 32);
+    cbuf_test("cbuf_init", &chdr, "");
+
+    bmp_init(bitmap, 4);
+    bmp_test("bmp_init", bitmap, 4, "00000000");
+
+    cbuf_reass_write(&chdr, 6, "abcdefghijklmnopqrstuvwxyz", 0, 26, bitmap, NULL, cbuf_copy_from_buffer);
+    cbuf_test("cbuf_reass_write (cbuf)", &chdr, "");
+    bmp_test("cbuf_reass_write (bitmap)", bitmap, 4, "03FFFFFF");
+
+    cbuf_write_string(&chdr, "ASDFGH");
+    cbuf_test("cbuf_write (cbuf)", &chdr, "ASDFGH");
+    bmp_test("cbuf_write (bitmap)", bitmap, 4, "03FFFFFF");
+
+    cbuf_pop(&chdr, 6);
+    cbuf_test("cbuf_pop (cbuf)", &chdr, "");
+    bmp_test("cbuf_pop (bitmap)", bitmap, 4, "03FFFFFF");
+
+    cbuf_reass_write(&chdr, 26, "!@#$^&", 0, 6, bitmap, NULL, cbuf_copy_from_buffer);
+    cbuf_test("cbuf_reass_write (cbuf)", &chdr, "");
+    bmp_test("cbuf_reass_write (bitmap)", bitmap, 4, "FFFFFFFF");
+
+    printf("Count Set: %d (should be at least 32)\n", (int) cbuf_reass_count_set(&chdr, 0, bitmap, 32));
+
+    cbuf_reass_merge(&chdr, 32, bitmap);
+    cbuf_test("cbuf_reass_merge (cbuf)", &chdr, "abcdefghijklmnopqrstuvwxyz!@#$^&");
+    bmp_test("cbuf_reass_merge (bitmap)", bitmap, 4, "00000000");
+}
+
+int main(int argc, char** argv) {
+    test_bmp();
+    test_cbuf();
+    test_cbuf_2();
+
+    printf("%" PRIu32 " tests passed (out of %" PRIu32 ")\n", num_tests_passed, num_tests_passed + num_tests_failed);
+    if (num_tests_failed != 0) {
+        return EXIT_FAILURE;
+    }
+    return EXIT_SUCCESS;
+}
diff --git a/.lgtm.yml b/tools/CMakeLists.txt
similarity index 85%
copy from .lgtm.yml
copy to tools/CMakeLists.txt
index 9051e95..5822c05 100644
--- a/.lgtm.yml
+++ b/tools/CMakeLists.txt
@@ -1,5 +1,5 @@
 #
-#  Copyright (c) 2020, The OpenThread Authors.
+#  Copyright (c) 2022, The OpenThread Authors.
 #  All rights reserved.
 #
 #  Redistribution and use in source and binary forms, with or without
@@ -26,12 +26,6 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-extraction:
-  cpp:
-    prepare:
-      packages:
-        - cmake
-        - ninja-build
-    index:
-      build_command:
-        - THREAD_VERSION=1.2 NODE_MODE=rcp OT_NATIVE_IP=1 ./script/test build
+if(OT_PLATFORM STREQUAL "posix")
+    add_subdirectory(ot-fct)
+endif()
diff --git a/tools/harness-simulation/README.md b/tools/harness-simulation/README.md
index 1412e9c..602fca5 100644
--- a/tools/harness-simulation/README.md
+++ b/tools/harness-simulation/README.md
@@ -4,46 +4,47 @@
 
 SI (Sniffer Interface) is an implementation of the sniffer abstract class template `ISniffer`, which is used by the Thread Test Harness Software to sniff all packets sent by devices.
 
-Both OpenThread simulation and sniffer simulation are required to run on a POSIX environment. However, Harness has to be run on Windows, which is a non-POSIX environment. So both two systems are needed, and their setup procedures in detail are listed in the following sections. Either two machines or one machine running two (sub)systems (for example, VM, WSL) is feasible.
+Both OpenThread simulation and sniffer simulation are required to run on a POSIX environment. However, Harness has to be run on Windows, which is a non-POSIX environment. So two systems are needed, and their setup procedures in detail are listed in the following sections. Either two machines or one machine running two (sub)systems (for example, VM, WSL) is feasible.
 
 Platform developers should modify the THCI implementation and/or the SI implementation directly to match their platform (for example, the path of the OpenThread repository).
 
 ## POSIX Environment Setup
 
-1. Build OpenThread to generate standalone OpenThread simulation `ot-cli-ftd`. For example, run the following command in the top directory of OpenThread.
+1. Open the JSON format configuration file `tools/harness-simulation/posix/config.yml`:
+
+   - Edit the value of `ot_path` to the absolute path where the top directory of the OpenThread repository is located. For example, change the value of `ot_path` to `/home/<username>/repo/openthread`.
+   - For each entry in `ot_build.ot`, update the value of `number` to be the number of OT FTD simulations needed with the corresponding version.
+   - For each entry in `ot_build.otbr`, update the value of `number` to be the number of OTBR simulations needed with the corresponding version.
+   - The numbers above can be adjusted according to the requirement of test cases.
+   - Edit the value of `ssh.username` to the username to be used for connecting to the remote POSIX environment.
+   - Edit the value of `ssh.password` to the password corresponding to the username above.
+   - Edit the value of `discovery_ifname` to the network interface that the Harness will connect to.
+
+   Note that it may be time-consuming to build all versions of `ot-cli-ftd`s and OTBR Docker images especially on devices such as Raspberry Pis.
+
+2. Run the installation script.
+
    ```bash
-   $ script/cmake-build simulation
+   $ tools/harness-simulation/posix/install.sh
    ```
-   Then `ot-cli-ftd` is built in the directory `build/simulation/examples/apps/cli/`.
 
 ## Test Harness Environment Setup
 
-1. Double click the file `harness\install.bat` on the machine which installed Harness.
+1. Copy the directory `tools/harness-simulation` from the POSIX machine to the Windows machine, and then switch to that directory.
 
-2. Check the configuration file `C:\GRL\Thread1.2\Thread_Harness\simulation\config.py`
-
-   - Edit the value of `REMOTE_USERNAME` to the username expected to connect to on the remote POSIX environment.
-   - Edit the value of `REMOTE_PASSWORD` to the password corresponding to the username above.
-   - Edit the value of `REMOTE_OT_PATH` to the absolute path where the top directory of the OpenThread repository is located.
-
-3. Add the additional simulation device information in `harness\Web\data\deviceInputFields.xml` to `C:\GRL\Thread1.2\Web\data\deviceInputFields.xml`.
+2. Double click the file `harness\install.bat` on Windows.
 
 ## Run Test Harness on Simulation
 
-1. On POSIX machine, change directory to the top of OpenThread repository, and run the following commands.
+1. On the POSIX machine, change directory to the top of the OpenThread repository, and run the following commands.
 
    ```bash
    $ cd tools/harness-simulation/posix
-   $ python harness_dev_discovery.py \
-         --interface=eth0            \
-         --ot1.1=24                  \
-         --sniffer=2
+   $ ./launch_testbed.py -c config.yml
    ```
 
-   It starts 24 OT FTD simulations and 2 sniffer simulations and can be discovered on eth0.
+   This example starts several OT FTD simulations, OTBR simulations, and sniffer simulations and can be discovered on `eth0`. The number of each type of simulation is specified in the configuration file `config.yml`.
 
-   The arguments can be adjusted according to the requirement of test cases.
-
-2. Run Test Harness. The information field of the device is encoded as `<node_id>@<ip_addr>`. Choose the proper device as the DUT accordingly.
+2. Run the Test Harness. The information field of the device is encoded as `<tag>_<node_id>@<ip_addr>`. Choose the desired device as the DUT.
 
 3. Select one or more test cases to start the test.
diff --git a/tools/harness-simulation/harness/Thread_Harness/Sniffer/SimSniffer.py b/tools/harness-simulation/harness/Thread_Harness/Sniffer/SimSniffer.py
index fffdc83..c5c55db 100644
--- a/tools/harness-simulation/harness/Thread_Harness/Sniffer/SimSniffer.py
+++ b/tools/harness-simulation/harness/Thread_Harness/Sniffer/SimSniffer.py
@@ -27,31 +27,29 @@
 # POSSIBILITY OF SUCH DAMAGE.
 #
 
+import grpc
 import ipaddress
+import itertools
+import json
 import netifaces
-import os
-import paramiko
 import select
 import socket
 import struct
-import subprocess
+import threading
 import time
+import win32api
 import winreg as wr
 
-from ISniffer import ISniffer
+from GRLLibs.UtilityModules.ModuleHelper import ModuleHelper
+from GRLLibs.UtilityModules.SnifferManager import SnifferManager
+from GRLLibs.UtilityModules.TopologyManager import TopologyManager
+from Sniffer.ISniffer import ISniffer
 from THCI.OpenThread import watched
-from simulation.config import (
-    REMOTE_PORT,
-    REMOTE_USERNAME,
-    REMOTE_PASSWORD,
-    REMOTE_OT_PATH,
-    REMOTE_SNIFFER_OUTPUT_PREFIX,
-    EDITCAP_PATH,
-)
+from simulation.Sniffer.proto import sniffer_pb2
+from simulation.Sniffer.proto import sniffer_pb2_grpc
 
 DISCOVERY_ADDR = ('ff02::114', 12345)
-
-IFNAME = 'WLAN'
+IFNAME = ISniffer.ethernet_interface_name
 
 SCAN_TIME = 3
 
@@ -63,17 +61,128 @@
 WINREG_KEY = r'SYSTEM\CurrentControlSet\Control\Network\{4d36e972-e325-11ce-bfc1-08002be10318}'
 
 
+# When Harness requires an RF shield box, it will pop up a message box via `ModuleHelper.UIMsgBox`
+# Replace the function to add and remove an RF enclosure simulation automatically without popping up a window
+def UIMsgBoxDecorator(UIMsgBox, replaceFuncs):
+
+    @staticmethod
+    def UIMsgBoxWrapper(msg='Confirm ??',
+                        title='User Input Required',
+                        inputRequired=False,
+                        default='',
+                        choices=None,
+                        timeout=None,
+                        isPrompt=False):
+        func = replaceFuncs.get((msg, title))
+        if func is None:
+            return UIMsgBox(msg, title, inputRequired, default, choices, timeout, isPrompt)
+        else:
+            return func()
+
+    return UIMsgBoxWrapper
+
+
+class DeviceManager:
+
+    def __init__(self):
+        deviceManager = TopologyManager.m_DeviceManager
+        self._devices = {'DUT': deviceManager.AutoDUTDeviceObject.THCI_Object}
+        for device_obj in deviceManager.DeviceList.values():
+            if device_obj.IsDeviceUsed:
+                self._devices[device_obj.DeviceTopologyInfo] = device_obj.THCI_Object
+
+    def __getitem__(self, deviceName):
+        device = self._devices.get(deviceName)
+        if device is None:
+            raise KeyError('Device Name "%s" not found' % deviceName)
+        return device
+
+
+class RFEnclosureManager:
+
+    def __init__(self, deviceNames1, deviceNames2, snifferPartitionId):
+        self.deviceNames1 = deviceNames1
+        self.deviceNames2 = deviceNames2
+        self.snifferPartitionId = snifferPartitionId
+
+    def placeRFEnclosure(self):
+        devices = DeviceManager()
+        self._partition1 = [devices[role] for role in self.deviceNames1]
+        self._partition2 = [devices[role] for role in self.deviceNames2]
+        sniffer_denied_partition = self._partition1 if self.snifferPartitionId == 2 else self._partition2
+
+        for device1 in self._partition1:
+            for device2 in self._partition2:
+                device1.addBlockedNodeId(device2.node_id)
+                device2.addBlockedNodeId(device1.node_id)
+
+        for sniffer in SnifferManager.SnifferObjects.values():
+            if sniffer.isSnifferCapturing():
+                sniffer.filterNodes(device.node_id for device in sniffer_denied_partition)
+
+    def removeRFEnclosure(self):
+        for device in itertools.chain(self._partition1, self._partition2):
+            device.clearBlockedNodeIds()
+
+        for sniffer in SnifferManager.SnifferObjects.values():
+            if sniffer.isSnifferCapturing():
+                sniffer.filterNodes(())
+
+        self._partition1 = None
+        self._partition2 = None
+
+
 class SimSniffer(ISniffer):
+    replaced_msgbox = False
 
     @watched
     def __init__(self, **kwargs):
         self.channel = kwargs.get('channel')
-        self.ipaddr = kwargs.get('addressofDevice')
+        self.addr_port = kwargs.get('addressofDevice')
         self.is_active = False
         self._local_pcapng_location = None
-        self._ssh = None
-        self._remote_pcap_location = None
-        self._remote_pid = None
+
+        if self.addr_port is not None:
+            # Replace `ModuleHelper.UIMsgBox` only when simulation devices exist
+            self._replaceMsgBox()
+
+            self._sniffer = grpc.insecure_channel(self.addr_port)
+            self._stub = sniffer_pb2_grpc.SnifferStub(self._sniffer)
+
+            # Close the sniffer only when Harness exits
+            win32api.SetConsoleCtrlHandler(self.__disconnect, True)
+
+    @watched
+    def _replaceMsgBox(self):
+        # Replace the function only once
+        if SimSniffer.replaced_msgbox:
+            return
+        SimSniffer.replaced_msgbox = True
+
+        test_9_2_9_leader = RFEnclosureManager(['Router_1', 'Router_2'], ['DUT', 'Commissioner'], 1)
+        test_9_2_9_router = RFEnclosureManager(['DUT', 'Router_2'], ['Leader', 'Commissioner'], 1)
+        test_9_2_10_router = RFEnclosureManager(['Leader', 'Commissioner'], ['DUT', 'MED_1', 'SED_1'], 2)
+
+        # Alter the behavior of `ModuleHelper.UIMsgBox` only when it comes to the following test cases:
+        #   - Leader 9.2.9
+        #   - Router 9.2.9
+        #   - Router 9.2.10
+        ModuleHelper.UIMsgBox = UIMsgBoxDecorator(
+            ModuleHelper.UIMsgBox,
+            replaceFuncs={
+                ("Place [Router1, Router2 and Sniffer] <br/> or <br/>[DUT and Commissioner] <br/>in an RF enclosure ", "Shield Devices"):
+                    test_9_2_9_leader.placeRFEnclosure,
+                ("Remove [Router1, Router2 and Sniffer] <br/> or <br/>[DUT and Commissioner] <br/>from RF enclosure ", "Unshield Devices"):
+                    test_9_2_9_leader.removeRFEnclosure,
+                ("Place [DUT,Router2 and sniffer] <br/> or <br/>[Leader and Commissioner] <br/>in an RF enclosure ", "Shield Devices"):
+                    test_9_2_9_router.placeRFEnclosure,
+                ("Remove [DUT, Router2 and sniffer] <br/> or <br/>[Leader and Commissioner] <br/>from RF enclosure ", "Unshield Devices"):
+                    test_9_2_9_router.removeRFEnclosure,
+                ("Place the <br/> [Leader and Commissioner] devices <br/> in an RF enclosure ", "Shield Devices"):
+                    test_9_2_10_router.placeRFEnclosure,
+                ("Remove <br/>[Leader and Commissioner] <br/>from RF enclosure ", "Unshield Devices"):
+                    test_9_2_10_router.removeRFEnclosure,
+            })
 
     def __repr__(self):
         return '%r' % self.__dict__
@@ -130,13 +239,14 @@
         start = time.time()
         while time.time() - start < SCAN_TIME:
             if select.select([sock], [], [], 1)[0]:
-                addr, _ = sock.recvfrom(1024)
-                devs.add(addr)
+                data, _ = sock.recvfrom(1024)
+                data = json.loads(data)
+                devs.add((data['add'], data['por']))
             else:
                 # Re-send the request, due to unreliability of UDP especially on WLAN
                 sock.sendto(('Sniffer').encode(), DISCOVERY_ADDR)
 
-        devs = [SimSniffer(addressofDevice=addr, channel=None) for addr in devs]
+        devs = [SimSniffer(addressofDevice=self._encode_address_port(addr, port), channel=None) for addr, port in devs]
         self.log('List of SimSniffers: %r', devs)
 
         return devs
@@ -145,53 +255,52 @@
     def startSniffer(self, channelToCapture, captureFileLocation, includeEthernet=False):
         self.channel = channelToCapture
         self._local_pcapng_location = captureFileLocation
-        self._remote_pcap_location = os.path.join(REMOTE_SNIFFER_OUTPUT_PREFIX, self.ipaddr.split('@')[0] + '.pcap')
 
-        self._ssh = paramiko.SSHClient()
-        self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-        remote_ip = self.ipaddr.split('@')[1]
-        self._ssh.connect(remote_ip, port=REMOTE_PORT, username=REMOTE_USERNAME, password=REMOTE_PASSWORD)
+        response = self._stub.Start(sniffer_pb2.StartRequest(channel=self.channel, includeEthernet=includeEthernet))
+        if response.status != sniffer_pb2.OK:
+            raise RuntimeError('startSniffer error: %s' % sniffer_pb2.Status.Name(response.status))
 
-        _, stdout, _ = self._ssh.exec_command(
-            'echo $$ && exec python3 %s -o %s -c %d' %
-            (os.path.join(REMOTE_OT_PATH, 'tools/harness-simulation/posix/sniffer_sim/sniffer.py'),
-             self._remote_pcap_location, self.channel))
-        self._remote_pid = int(stdout.readline())
-
-        self.log('local pcapng location = %s', self._local_pcapng_location)
-        self.log('remote pcap location = %s', self._remote_pcap_location)
-        self.log('remote pid = %d', self._remote_pid)
+        self._thread = threading.Thread(target=self._file_sync_main_loop)
+        self._thread.setDaemon(True)
+        self._thread.start()
 
         self.is_active = True
 
     @watched
+    def _file_sync_main_loop(self):
+        with open(self._local_pcapng_location, 'wb') as f:
+            for response in self._stub.TransferPcapng(sniffer_pb2.TransferPcapngRequest()):
+                f.write(response.content)
+                f.flush()
+
+    @watched
     def stopSniffer(self):
         if not self.is_active:
             return
+
+        response = self._stub.Stop(sniffer_pb2.StopRequest())
+        if response.status != sniffer_pb2.OK:
+            raise RuntimeError('stopSniffer error: %s' % sniffer_pb2.Status.Name(response.status))
+
+        self._thread.join()
+
         self.is_active = False
 
-        assert self._ssh is not None
-        self._ssh.exec_command('kill -s TERM %d' % self._remote_pid)
-        # Wait to make sure the file is closed
-        time.sleep(3)
+    @watched
+    def filterNodes(self, nodeids):
+        if not self.is_active:
+            return
 
-        # Truncate suffix from .pcapng to .pcap
-        local_pcap_location = self._local_pcapng_location[:-2]
+        request = sniffer_pb2.FilterNodesRequest()
+        request.nodeids.extend(nodeids)
 
-        with self._ssh.open_sftp() as sftp:
-            sftp.get(self._remote_pcap_location, local_pcap_location)
+        response = self._stub.FilterNodes(request)
+        if response.status != sniffer_pb2.OK:
+            raise RuntimeError('filterNodes error: %s' % sniffer_pb2.Status.Name(response.status))
 
-        self._ssh.close()
-
-        cmd = [EDITCAP_PATH, '-F', 'pcapng', local_pcap_location, self._local_pcapng_location]
-        self.log('running editcap: %r', cmd)
-        subprocess.Popen(cmd).wait()
-        self.log('editcap done')
-
-        self._local_pcapng_location = None
-        self._ssh = None
-        self._remote_pcap_location = None
-        self._remote_pid = None
+    def __disconnect(self, dwCtrlType):
+        if self._sniffer is not None:
+            self._sniffer.close()
 
     @watched
     def setChannel(self, channelToCapture):
@@ -211,7 +320,7 @@
 
     @watched
     def getSnifferAddress(self):
-        return self.ipaddr
+        return self.addr_port
 
     @watched
     def globalReset(self):
diff --git a/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_BR_Sim.py b/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_BR_Sim.py
new file mode 100644
index 0000000..17c8698
--- /dev/null
+++ b/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_BR_Sim.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2022, 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.
+#
+"""
+>> Thread Host Controller Interface
+>> Device : OpenThread_BR_Sim THCI
+>> Class : OpenThread_BR_Sim
+"""
+
+import ipaddress
+import logging
+import paramiko
+import pipes
+import sys
+import time
+
+from THCI.IThci import IThci
+from THCI.OpenThread import watched
+from THCI.OpenThread_BR import OpenThread_BR
+from simulation.config import load_config
+
+logging.getLogger('paramiko').setLevel(logging.WARNING)
+
+config = load_config()
+
+
+class SSHHandle(object):
+    # Unit: second
+    KEEPALIVE_INTERVAL = 30
+
+    def __init__(self, ip, port, username, password, docker_name):
+        self.ip = ip
+        self.port = int(port)
+        self.username = username
+        self.password = password
+        self.docker_name = docker_name
+        self.__handle = None
+
+        self.__connect()
+
+    def __connect(self):
+        self.close()
+
+        self.__handle = paramiko.SSHClient()
+        self.__handle.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+        try:
+            self.__handle.connect(self.ip, port=self.port, username=self.username, password=self.password)
+        except paramiko.AuthenticationException:
+            if not self.password:
+                self.__handle.get_transport().auth_none(self.username)
+            else:
+                raise Exception('Password error')
+
+        # Avoid SSH disconnection after idle for a long time
+        self.__handle.get_transport().set_keepalive(self.KEEPALIVE_INTERVAL)
+
+    def close(self):
+        if self.__handle is not None:
+            self.__handle.close()
+            self.__handle = None
+
+    def bash(self, cmd, timeout):
+        # It is necessary to quote the command when there is stdin/stdout redirection
+        cmd = pipes.quote(cmd)
+
+        retry = 3
+        for i in range(retry):
+            try:
+                stdin, stdout, stderr = self.__handle.exec_command('docker exec %s bash -c %s' %
+                                                                   (self.docker_name, cmd),
+                                                                   timeout=timeout)
+                stdout._set_mode('rb')
+
+                sys.stderr.write(stderr.read())
+                output = [r.rstrip() for r in stdout.readlines()]
+                return output
+
+            except paramiko.SSHException:
+                if i < retry - 1:
+                    print('SSH connection is lost, try reconnect after 1 second.')
+                    time.sleep(1)
+                    self.__connect()
+                else:
+                    raise ConnectionError('SSH connection is lost')
+
+
+class OpenThread_BR_Sim(OpenThread_BR):
+
+    def _getHandle(self):
+        self.log('SSH connecting ...')
+        return SSHHandle(self.ssh_ip, self.telnetPort, self.telnetUsername, self.telnetPassword, self.docker_name)
+
+    @watched
+    def _parseConnectionParams(self, params):
+        discovery_add = params.get('SerialPort')
+        if '@' not in discovery_add:
+            raise ValueError('%r in the field `add` is invalid' % discovery_add)
+
+        self.docker_name, self.ssh_ip = discovery_add.split('@')
+        self.tag, self.node_id = self.docker_name.split('_')
+        self.node_id = int(self.node_id)
+        # Let it crash if it is an invalid IP address
+        ipaddress.ip_address(self.ssh_ip)
+
+        self.connectType = 'ip'
+        self.telnetIp = self.port = discovery_add
+
+        global config
+        ssh = config['ssh']
+        self.telnetPort = ssh['port']
+        self.telnetUsername = ssh['username']
+        self.telnetPassword = ssh['password']
+
+        self.extraParams = {
+            'cmd-start-otbr-agent': 'service otbr-agent start',
+            'cmd-stop-otbr-agent': 'service otbr-agent stop',
+            'cmd-restart-otbr-agent': 'service otbr-agent restart',
+            'cmd-restart-radvd': 'service radvd stop; service radvd start',
+        }
+
+
+assert issubclass(OpenThread_BR_Sim, IThci)
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 e0db9bd..18c622e 100644
--- a/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_Sim.py
+++ b/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_Sim.py
@@ -33,32 +33,41 @@
 """
 
 import ipaddress
-import os
 import paramiko
 import socket
 import time
+import win32api
 
-from IThci import IThci
-from OpenThread import OpenThreadTHCI, watched
-from simulation.config import REMOTE_OT_PATH
+from simulation.config import load_config
+from THCI.IThci import IThci
+from THCI.OpenThread import OpenThreadTHCI, watched
+
+config = load_config()
+ot_subpath = {item['tag']: item['subpath'] for item in config['ot_build']['ot']}
 
 
 class SSHHandle(object):
+    KEEPALIVE_INTERVAL = 30
 
     def __init__(self, ip, port, username, password, device, node_id):
-        ipaddress.ip_address(ip)
         self.ip = ip
         self.port = int(port)
         self.username = username
         self.password = password
+        self.node_id = node_id
         self.__handle = None
         self.__stdin = None
         self.__stdout = None
-        self.__connect(device, node_id)
+
+        self.__connect(device)
+
+        # Close the SSH connection only when Harness exits
+        win32api.SetConsoleCtrlHandler(self.__disconnect, True)
 
     @watched
-    def __connect(self, device, node_id):
-        self.close()
+    def __connect(self, device):
+        if self.__handle is not None:
+            return
 
         self.__handle = paramiko.SSHClient()
         self.__handle.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -71,58 +80,66 @@
             else:
                 raise Exception('Password error')
 
-        self.__stdin, self.__stdout, _ = self.__handle.exec_command(device + ' ' + str(node_id), get_pty=True)
+        # Avoid SSH connection lost after inactivity for a while
+        self.__handle.get_transport().set_keepalive(self.KEEPALIVE_INTERVAL)
+
+        self.__stdin, self.__stdout, _ = self.__handle.exec_command(device + ' ' + str(self.node_id))
+
+        # Receive the output in non-blocking mode
         self.__stdout.channel.setblocking(0)
 
-        # Wait some time for initiation
-        time.sleep(0.1)
+        # Some commands such as `udp send <ip> -x <hex>` send binary data
+        # The UDP packet recevier will output the data in binary to stdout
+        self.__stdout._set_mode('rb')
 
-    @watched
-    def close(self):
+    def __disconnect(self, dwCtrlType):
         if self.__handle is None:
             return
-        self.__stdin.write('exit\n')
-        # Wait some time for termination
-        time.sleep(0.1)
+
+        # Exit ot-cli-ftd and close the SSH connection
+        self.send('exit\n')
+        self.__stdin.close()
+        self.__stdout.close()
         self.__handle.close()
-        self.__stdin = None
-        self.__stdout = None
-        self.__handle = None
+
+    def close(self):
+        # Do nothing, because disconnecting and then connecting will automatically factory reset all states
+        # compared to real devices, which is not the intended behavior
+        pass
 
     def send(self, cmd):
         self.__stdin.write(cmd)
+        self.__stdin.flush()
 
     def recv(self):
-        try:
-            return self.__stdout.readline().rstrip()
-        except socket.timeout:
-            return ''
+        outputs = []
+        while True:
+            try:
+                outputs.append(self.__stdout.read(1))
+            except socket.timeout:
+                break
+        return ''.join(outputs)
 
     def log(self, fmt, *args):
         try:
             msg = fmt % args
-            print('%s - %s - %s' % (self.port, time.strftime('%b %d %H:%M:%S'), msg))
+            print('%d@%s - %s - %s' % (self.node_id, self.ip, time.strftime('%b %d %H:%M:%S'), msg))
         except Exception:
             pass
 
 
 class OpenThread_Sim(OpenThreadTHCI, IThci):
-    DEFAULT_COMMAND_TIMEOUT = 20
-
     __handle = None
 
-    device = os.path.join(REMOTE_OT_PATH, 'build/simulation/examples/apps/cli/ot-cli-ftd')
-
     @watched
     def _connect(self):
+        self.__lines = []
+
         # Only actually connect once.
         if self.__handle is None:
-            assert self.connectType == 'ip'
-            assert '@' in self.telnetIp
             self.log('SSH connecting ...')
-            node_id, ssh_ip = self.telnetIp.split('@')
-            self.__handle = SSHHandle(ssh_ip, self.telnetPort, self.telnetUsername, self.telnetPassword, self.device,
-                                      node_id)
+            self.__handle = SSHHandle(self.ssh_ip, self.telnetPort, self.telnetUsername, self.telnetPassword,
+                                      self.device, self.node_id)
 
         self.log('connected to %s successfully', self.telnetIp)
 
@@ -130,9 +147,45 @@
     def _disconnect(self):
         pass
 
+    @watched
+    def _parseConnectionParams(self, params):
+        discovery_add = params.get('SerialPort')
+        if '@' not in discovery_add:
+            raise ValueError('%r in the field `add` is invalid' % discovery_add)
+
+        prefix, self.ssh_ip = discovery_add.split('@')
+        self.tag, self.node_id = prefix.split('_')
+        self.node_id = int(self.node_id)
+        # Let it crash if it is an invalid IP address
+        ipaddress.ip_address(self.ssh_ip)
+
+        # Do not use `os.path.join` as it uses backslash as the separator on Windows
+        global config
+        self.device = '/'.join([config['ot_path'], ot_subpath[self.tag], 'examples/apps/cli/ot-cli-ftd'])
+
+        self.connectType = 'ip'
+        self.telnetIp = self.port = discovery_add
+
+        ssh = config['ssh']
+        self.telnetPort = ssh['port']
+        self.telnetUsername = ssh['username']
+        self.telnetPassword = ssh['password']
+
     def _cliReadLine(self):
-        tail = self.__handle.recv()
-        return tail if tail else None
+        if len(self.__lines) > 1:
+            return self.__lines.pop(0)
+
+        tail = ''
+        if len(self.__lines) != 0:
+            tail = self.__lines.pop()
+
+        tail += self.__handle.recv()
+
+        self.__lines += self._lineSepX.split(tail)
+        if len(self.__lines) > 1:
+            return self.__lines.pop(0)
+
+        return None
 
     def _cliWriteLine(self, line):
         self.__handle.send(line + '\n')
diff --git a/tools/harness-simulation/harness/Thread_Harness/simulation/Sniffer/__init__.py b/tools/harness-simulation/harness/Thread_Harness/simulation/Sniffer/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/harness-simulation/harness/Thread_Harness/simulation/Sniffer/__init__.py
diff --git a/tools/harness-simulation/harness/Thread_Harness/simulation/Sniffer/proto/__init__.py b/tools/harness-simulation/harness/Thread_Harness/simulation/Sniffer/proto/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/harness-simulation/harness/Thread_Harness/simulation/Sniffer/proto/__init__.py
diff --git a/tools/harness-simulation/harness/Thread_Harness/simulation/config.py b/tools/harness-simulation/harness/Thread_Harness/simulation/config.py
index 44e866e..61bc379 100644
--- a/tools/harness-simulation/harness/Thread_Harness/simulation/config.py
+++ b/tools/harness-simulation/harness/Thread_Harness/simulation/config.py
@@ -27,12 +27,13 @@
 # POSSIBILITY OF SUCH DAMAGE.
 #
 
-REMOTE_PORT = 22
-REMOTE_USERNAME = 'pi'
-REMOTE_PASSWORD = 'raspberry'
+import os
+import yaml
 
-REMOTE_SNIFFER_OUTPUT_PREFIX = '/tmp/'
+CONFIG_PATH = r'%s\GRL\Thread1.2\Thread_Harness\simulation\config.yml' % os.environ['systemdrive']
 
-REMOTE_OT_PATH = '/home/pi/work/src/openthread-pr/'
 
-EDITCAP_PATH = r'C:\Program Files (x86)\Wireshark_Thread\editcap.exe'
+def load_config():
+    with open(CONFIG_PATH, 'rt') as f:
+        config = yaml.safe_load(f)
+    return config
diff --git a/tools/harness-simulation/harness/Web/data/deviceInputFields.xml b/tools/harness-simulation/harness/Web/data/deviceInputFields.xml
index a9e09d4..3191ac6 100644
--- a/tools/harness-simulation/harness/Web/data/deviceInputFields.xml
+++ b/tools/harness-simulation/harness/Web/data/deviceInputFields.xml
@@ -1,10 +1,8 @@
 <DEVICE_FIELDS>
     <DEVICE name="OpenThread_Sim" thumbnail="OpenThread.png" description = "OpenThread Simulation" THCI="OpenThread_Sim">
-        <ITEM label="Serial Line"
-              type="text"
-              forParam="SerialPort"
-              validation="COM"
-              hint="eg: COM1">COM1
-        </ITEM>
+        <ITEM type="text" forParam="SerialPort">UNSPECIFIED</ITEM>
+    </DEVICE>
+    <DEVICE name="OpenThread_BR_Sim" thumbnail="OpenThread_BR.png" description = "OpenThread BR Simulation" THCI="OpenThread_BR_Sim">
+        <ITEM type="text" forParam="SerialPort">UNSPECIFIED</ITEM>
     </DEVICE>
 </DEVICE_FIELDS>
diff --git a/tools/harness-simulation/harness/Web/data/updateDeviceFields.py b/tools/harness-simulation/harness/Web/data/updateDeviceFields.py
new file mode 100644
index 0000000..a683002
--- /dev/null
+++ b/tools/harness-simulation/harness/Web/data/updateDeviceFields.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2022, 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.
+#
+
+# This script merges the device fields in the argument file to `deviceInputFields.xml` in Harness
+# If a device field appears in both files, it will only keep that in the argument file
+
+import os
+import sys
+import xml.etree.ElementTree as ET
+
+HARNESS_XML_PATH = r'%s\GRL\Thread1.2\Web\data\deviceInputFields.xml' % os.environ['systemdrive']
+
+
+def main():
+    tree = ET.parse(HARNESS_XML_PATH)
+    root = tree.getroot()
+    added = ET.parse(sys.argv[1]).getroot()
+
+    added_names = set(device.attrib['name'] for device in added.iter('DEVICE'))
+    # If some devices already exist, remove them first, and then add them back in case of update
+    removed_devices = filter(lambda x: x.attrib['name'] in added_names, root.iter('DEVICE'))
+
+    for device in removed_devices:
+        root.remove(device)
+    for device in added.iter('DEVICE'):
+        root.append(device)
+
+    tree.write(HARNESS_XML_PATH)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tools/harness-simulation/harness/install.bat b/tools/harness-simulation/harness/install.bat
index 0b4a898..99be41c 100644
--- a/tools/harness-simulation/harness/install.bat
+++ b/tools/harness-simulation/harness/install.bat
@@ -1,2 +1,45 @@
-xcopy /E /Y Thread_Harness %systemdrive%\GRL\Thread1.2\Thread_Harness
-copy /Y ..\..\harness-thci\OpenThread.py %systemdrive%\GRL\Thread1.2\Thread_Harness\THCI
+:: Copyright (c) 2022, 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 THREADDIR=%systemdrive%\GRL\Thread1.2
+xcopy /E /Y Thread_Harness %THREADDIR%\Thread_Harness
+copy /Y ..\..\harness-thci\OpenThread.py %THREADDIR%\Thread_Harness\THCI
+copy /Y ..\..\harness-thci\OpenThread_BR.py %THREADDIR%\Thread_Harness\THCI
+copy /Y ..\..\harness-thci\OpenThread.png %THREADDIR%\Web\images
+copy /Y ..\..\harness-thci\OpenThread_BR.png %THREADDIR%\Web\images
+copy /Y ..\posix\config.yml %THREADDIR%\Thread_Harness\simulation
+xcopy /E /Y ..\posix\sniffer_sim\proto %THREADDIR%\Thread_Harness\simulation\Sniffer\proto
+
+%THREADDIR%\Python27\python.exe -m pip install --upgrade pip
+%THREADDIR%\Python27\python.exe -m pip install -r requirements.txt
+
+%THREADDIR%\Python27\python.exe Web\data\updateDeviceFields.py Web\data\deviceInputFields.xml
+
+set BASEDIR=%THREADDIR%\Thread_Harness
+%systemdrive%\GRL\Thread1.2\Python27\python.exe -m grpc_tools.protoc -I%BASEDIR% --python_out=%BASEDIR% --grpc_python_out=%BASEDIR% simulation/Sniffer/proto/sniffer.proto
+
+pause
diff --git a/tools/harness-simulation/harness/requirements.txt b/tools/harness-simulation/harness/requirements.txt
new file mode 100644
index 0000000..179c044
--- /dev/null
+++ b/tools/harness-simulation/harness/requirements.txt
@@ -0,0 +1,3 @@
+grpcio==1.20.1
+grpcio-tools==1.20.1
+PyYAML==5.4.1
diff --git a/tools/harness-simulation/posix/config.yml b/tools/harness-simulation/posix/config.yml
new file mode 100644
index 0000000..11b29bc
--- /dev/null
+++ b/tools/harness-simulation/posix/config.yml
@@ -0,0 +1,91 @@
+ot_path: "/home/pi/repo/openthread"
+ot_build:
+  max_number: 64
+  ot:
+  - tag: OT11
+    version: '1.1'
+    number: 33
+    subpath: build/ot11/simulation
+    cflags:
+    - "-DOPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS=8"
+    options:
+    - "-DOT_REFERENCE_DEVICE=ON"
+    - "-DOT_COMMISSIONER=ON"
+    - "-DOT_JOINER=ON"
+  - tag: OT12
+    version: '1.2'
+    number: 10
+    subpath: build/ot12/simulation
+    cflags:
+    - "-DOPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS=8"
+    options:
+    - "-DOT_REFERENCE_DEVICE=ON"
+    - "-DOT_DUA=ON"
+    - "-DOT_MLR=ON"
+    - "-DOT_COMMISSIONER=ON"
+    - "-DOT_JOINER=ON"
+    - "-DOT_CSL_RECEIVER=ON"
+    - "-DOT_LINK_METRICS_SUBJECT=ON"
+    - "-DOT_LINK_METRICS_INITIATOR=ON"
+  - tag: OT13
+    version: '1.3'
+    number: 10
+    subpath: build/ot13/simulation
+    cflags:
+    - "-DOPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS=8"
+    options:
+    - "-DOT_REFERENCE_DEVICE=ON"
+    - "-DOT_DUA=ON"
+    - "-DOT_MLR=ON"
+    - "-DOT_COMMISSIONER=ON"
+    - "-DOT_JOINER=ON"
+    - "-DOT_CSL_RECEIVER=ON"
+    - "-DOT_LINK_METRICS_SUBJECT=ON"
+    - "-DOT_LINK_METRICS_INITIATOR=ON"
+  otbr:
+  - tag: OTBR12
+    version: '1.2'
+    number: 4
+    docker_image: otbr-reference-device-1.2
+    build_args:
+    - REFERENCE_DEVICE=1
+    - BORDER_ROUTING=0
+    - BACKBONE_ROUTER=1
+    - NAT64=0
+    - WEB_GUI=0
+    - REST_API=0
+    - OT_COMMISSIONER=1
+    options:
+    - "-DOTBR_DUA_ROUTING=ON"
+    - "-DOT_DUA=ON"
+    - "-DOT_MLR=ON"
+    rcp_subpath: build/ot12/simulation
+    rcp_options:
+    - "-DOT_LINK_METRICS_SUBJECT=ON"
+  - tag: OTBR13
+    version: '1.3'
+    number: 4
+    docker_image: otbr-reference-device-1.3
+    build_args:
+    - REFERENCE_DEVICE=1
+    - BORDER_ROUTING=1
+    - BACKBONE_ROUTER=1
+    - NAT64=0
+    - WEB_GUI=0
+    - REST_API=0
+    - EXTERNAL_COMMISSIONER=1
+    options:
+    - "-DOTBR_DUA_ROUTING=ON"
+    - "-DOT_DUA=ON"
+    - "-DOT_MLR=ON"
+    rcp_subpath: build/ot13/simulation
+    rcp_options:
+    - "-DOT_LINK_METRICS_SUBJECT=ON"
+ssh:
+  username: pi
+  password: raspberry
+  port: 22
+sniffer:
+  number: 2
+  server_port_base: 50051
+discovery_ifname: eth0
diff --git a/tools/harness-simulation/posix/etc/Dockerfile b/tools/harness-simulation/posix/etc/Dockerfile
new file mode 100644
index 0000000..7363493
--- /dev/null
+++ b/tools/harness-simulation/posix/etc/Dockerfile
@@ -0,0 +1,119 @@
+#  Copyright (c) 2022, 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.
+#
+
+ARG BASE_IMAGE=ubuntu:focal
+FROM ${BASE_IMAGE}
+
+ARG INFRA_IF_NAME
+ARG BORDER_ROUTING
+ARG BACKBONE_ROUTER
+ARG OTBR_OPTIONS
+ARG EXTERNAL_COMMISSIONER
+ARG DNS64
+ARG NAT64
+ARG NAT64_SERVICE
+ARG REFERENCE_DEVICE
+ARG REST_API
+ARG WEB_GUI
+ARG MDNS
+
+ENV INFRA_IF_NAME=${INFRA_IF_NAME:-eth0}
+ENV BORDER_ROUTING=${BORDER_ROUTING:-1}
+ENV BACKBONE_ROUTER=${BACKBONE_ROUTER:-1}
+ENV OTBR_MDNS=${MDNS:-mDNSResponder}
+ENV OTBR_OPTIONS=${OTBR_OPTIONS}
+ENV EXTERNAL_COMMISSIONER=${EXTERNAL_COMMISSIONER:-1}
+ENV DEBIAN_FRONTEND noninteractive
+ENV PLATFORM ubuntu
+ENV REFERENCE_DEVICE=${REFERENCE_DEVICE:-0}
+ENV NAT64=${NAT64:-1}
+ENV NAT64_SERVICE=${NAT64_SERVICE:-tayga}
+ENV DNS64=${DNS64:-0}
+ENV WEB_GUI=${WEB_GUI:-1}
+ENV REST_API=${REST_API:-1}
+ENV DOCKER 1
+
+RUN env
+
+COPY . /app
+WORKDIR /app
+
+# Required during build or run
+ENV OTBR_DOCKER_REQS sudo python2 python3 python-is-python2
+
+# Required during build, could be removed
+ENV OTBR_DOCKER_DEPS git ca-certificates python3-pip wget
+
+# Required during run python scripts
+ENV OTBR_PYTHON_REQS zeroconf
+
+# Required and installed during build (script/bootstrap), could be removed
+ENV OTBR_BUILD_DEPS apt-utils build-essential psmisc ninja-build cmake ca-certificates \
+  libreadline-dev libncurses-dev libcpputest-dev libdbus-1-dev libavahi-common-dev \
+  libavahi-client-dev libboost-dev libboost-filesystem-dev libboost-system-dev \
+  libnetfilter-queue-dev
+
+RUN apt-get update \
+  && cp -r ./root/. / \
+  && rm -rf ./root \
+  && apt-get install --no-install-recommends -y $OTBR_DOCKER_REQS $OTBR_DOCKER_DEPS \
+  && wget -P /tmp "https://bootstrap.pypa.io/pip/2.7/get-pip.py" \
+  && python2 /tmp/get-pip.py \
+  && pip2 install -r /tmp/requirements.txt \
+  && pip3 install $OTBR_PYTHON_REQS \
+  && ln -fs /usr/share/zoneinfo/UTC /etc/localtime \
+  && ([ "${EXTERNAL_COMMISSIONER}" != "1" ] || ( \
+    git clone https://github.com/openthread/ot-commissioner.git --recurse-submodules --shallow-submodules --depth=1 \
+    && cd ot-commissioner \
+    && ./script/bootstrap.sh \
+    && mkdir -p build \
+    && cd build \
+    && cmake -GNinja -DCMAKE_INSTALL_PREFIX="/usr/local" -DOT_COMM_REFERENCE_DEVICE=ON .. \
+    && ninja \
+    && ninja install \
+    && cd /app \
+    && rm -rf ot-commissioner \
+  )) \
+  && ./script/bootstrap \
+  && ./script/setup \
+  && ([ "${DNS64}" = "0" ] || chmod 644 /etc/bind/named.conf.options) \
+  && mv ./script /tmp \
+  && mv ./etc /tmp \
+  && find . -delete \
+  && rm -rf /usr/include \
+  && mv /tmp/script . \
+  && mv /tmp/etc . \
+  && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $OTBR_DOCKER_DEPS \
+  && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $OTBR_BUILD_DEPS  \
+  && rm -rf /var/lib/apt/lists/* \
+  && rm -rf /tmp/* \
+  && sed -i "s/\/root/\/home\/pi/g" /etc/passwd
+  # The command above changes root home directory to /home/pi
+
+ENTRYPOINT ["/app/etc/docker/docker_entrypoint.sh"]
+
+EXPOSE 80
diff --git a/tools/harness-simulation/posix/etc/commissionerd b/tools/harness-simulation/posix/etc/commissionerd
new file mode 100755
index 0000000..6a264e1
--- /dev/null
+++ b/tools/harness-simulation/posix/etc/commissionerd
@@ -0,0 +1,100 @@
+#!/bin/sh
+#
+#  Copyright (c) 2022, 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.
+#
+### BEGIN INIT INFO
+# Provides:          commissionerd
+# Required-Start:
+# Required-Stop:
+# Should-Start:
+# Should-Stop:
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: OT-commissioner daemon
+# Description: OT-commissioner daemon
+### END INIT INFO
+
+set -e
+
+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+DESC="OT-commissioner daemon"
+NAME=commissionerd
+DAEMON=/usr/bin/python2
+PIDFILE=/var/run/commissionerd.pid
+
+# shellcheck source=/dev/null
+. /lib/lsb/init-functions
+# shellcheck source=/dev/null
+. /lib/init/vars.sh
+
+start_commissionerd()
+{
+    if [ -e $PIDFILE ]; then
+        if $0 status >/dev/null; then
+            log_success_msg "$DESC already started; not starting."
+            return
+        else
+            log_success_msg "Removing stale PID file $PIDFILE."
+            rm -f $PIDFILE
+        fi
+    fi
+
+    log_daemon_msg "Starting $DESC" "$NAME"
+    start-stop-daemon --start --quiet \
+        --pidfile $PIDFILE --make-pidfile \
+        -b --exec $DAEMON -- \
+        -u /usr/local/bin/commissionerd.py -c /usr/local/bin/commissioner-cli
+    log_end_msg $?
+}
+
+stop_commissionerd()
+{
+    log_daemon_msg "Stopping $DESC" "$NAME"
+    start-stop-daemon --stop --retry 5 --quiet --oknodo \
+        --pidfile $PIDFILE --remove-pidfile
+    log_end_msg $?
+}
+
+case "$1" in
+    start)
+        start_commissionerd
+        ;;
+    restart | reload | force-reload)
+        stop_commissionerd
+        start_commissionerd
+        ;;
+    stop | force-stop)
+        stop_commissionerd
+        ;;
+    status)
+        status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $?
+        ;;
+    *)
+        log_action_msg "Usage: /etc/init.d/$NAME {start | stop | status | restart | reload | force-reload}"
+        exit 2
+        ;;
+esac
diff --git a/tools/harness-simulation/posix/etc/requirements.txt b/tools/harness-simulation/posix/etc/requirements.txt
new file mode 100644
index 0000000..5804683
--- /dev/null
+++ b/tools/harness-simulation/posix/etc/requirements.txt
@@ -0,0 +1,3 @@
+pexpect==4.7.0
+ptyprocess==0.6.0
+pyserial==3.4
diff --git a/tools/harness-simulation/posix/etc/server.patch b/tools/harness-simulation/posix/etc/server.patch
new file mode 100644
index 0000000..0f29c95
--- /dev/null
+++ b/tools/harness-simulation/posix/etc/server.patch
@@ -0,0 +1,18 @@
+--- script/server	2022-08-15 11:41:53.915673348 +0800
++++ script/server2	2022-08-15 11:43:12.387100651 +0800
+@@ -50,6 +50,7 @@
+         systemctl is-active avahi-daemon || sudo systemctl start avahi-daemon || die 'Failed to start avahi!'
+         without WEB_GUI || systemctl is-active otbr-web || sudo systemctl start otbr-web || die 'Failed to start otbr-web!'
+         systemctl is-active otbr-agent || sudo systemctl start otbr-agent || die 'Failed to start otbr-agent!'
++        systemctl is-active commissionerd || sudo systemctl start commissionerd || die 'Failed to start commissionerd!'
+     elif have service; then
+         sudo service rsyslog status || sudo service rsyslog start || die 'Failed to start rsyslog!'
+         sudo service dbus status || sudo service dbus start || die 'Failed to start dbus!'
+@@ -58,6 +59,7 @@
+         sudo service avahi-daemon status || sudo service avahi-daemon start || die 'Failed to start avahi!'
+         sudo service otbr-agent status || sudo service otbr-agent start || die 'Failed to start otbr-agent!'
+         without WEB_GUI || sudo service otbr-web status || sudo service otbr-web start || die 'Failed to start otbr-web!'
++        sudo service commissionerd status || sudo service commissionerd start || die 'Failed to start commissionerd!'
+     else
+         die 'Unable to find service manager. Try script/console to start in console mode!'
+     fi
diff --git a/tools/harness-simulation/posix/harness_dev_discovery.py b/tools/harness-simulation/posix/harness_dev_discovery.py
deleted file mode 100644
index 1587c99..0000000
--- a/tools/harness-simulation/posix/harness_dev_discovery.py
+++ /dev/null
@@ -1,169 +0,0 @@
-#!/usr/bin/env python3
-#
-#  Copyright (c) 2022, 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.
-#
-
-import argparse
-import ctypes
-import ctypes.util
-import json
-import logging
-import os
-import socket
-import struct
-
-GROUP = 'ff02::114'
-PORT = 12345
-MAX_OT11_NUM = 33
-MAX_SNIFFER_NUM = 4
-
-
-def if_nametoindex(ifname: str) -> int:
-    libc = ctypes.CDLL(ctypes.util.find_library('c'))
-    ret = libc.if_nametoindex(ifname.encode('ascii'))
-    if not ret:
-        raise RuntimeError('Invalid interface name')
-    return ret
-
-
-def get_ipaddr(ifname: str) -> str:
-    for line in os.popen(f'ip addr list dev {ifname} | grep inet | grep global'):
-        addr = line.strip().split()[1]
-        return addr.split('/')[0]
-    raise RuntimeError(f'No IP address on dev {ifname}')
-
-
-def init_socket(ifname: str, group: str, port: int) -> socket.socket:
-    # Look up multicast group address in name server and find out IP version
-    addrinfo = socket.getaddrinfo(group, None)[0]
-    assert addrinfo[0] == socket.AF_INET6
-
-    # Create a socket
-    s = socket.socket(addrinfo[0], socket.SOCK_DGRAM)
-    s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, (ifname + '\0').encode('ascii'))
-
-    # Bind it to the port
-    s.bind((group, port))
-
-    group_bin = socket.inet_pton(addrinfo[0], addrinfo[4][0])
-    # Join group
-    interface_index = if_nametoindex(ifname)
-    mreq = group_bin + struct.pack('@I', interface_index)
-    s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
-
-    return s
-
-
-def advertise_ftd(s: socket.socket, dst, ven: str, ver: str, add: str, por: int, number: int):
-    # Node ID of ot-cli-ftd is 1-indexed
-    for i in range(1, number + 1):
-        info = {
-            'ven': ven,
-            'mod': f'{ven}_{i}',
-            'ver': ver,
-            'add': f'{i}@{add}',
-            'por': por,
-        }
-        logging.info('Advertise: %r', info)
-        s.sendto(json.dumps(info).encode('utf-8'), dst)
-
-
-def advertise_sniffer(s: socket.socket, dst, add: str, number: int):
-    for i in range(number):
-        info = 'Sniffer_%d@%s' % (i, add)
-        logging.info('Advertise: %r', info)
-        s.sendto(info.encode('utf-8'), dst)
-
-
-def main():
-    logging.basicConfig(level=logging.INFO)
-
-    # Parse arguments
-    parser = argparse.ArgumentParser()
-
-    # Determine the interface
-    parser.add_argument('-i',
-                        '--interface',
-                        dest='ifname',
-                        type=str,
-                        required=True,
-                        help='the interface used for discovery')
-
-    # Determine the number of OpenThread 1.1 FTD simulations to be "detected" and then initiated
-    parser.add_argument('--ot1.1',
-                        dest='ot11_num',
-                        type=int,
-                        required=False,
-                        default=0,
-                        help=f'the number of OpenThread FTD simulations, no more than {MAX_OT11_NUM}')
-
-    # Determine the number of sniffer simulations to be initiated and then detected
-    parser.add_argument('-s',
-                        '--sniffer',
-                        dest='sniffer_num',
-                        type=int,
-                        required=False,
-                        default=0,
-                        help=f'the number of sniffer simulations, no more than {MAX_SNIFFER_NUM}')
-
-    args = parser.parse_args()
-
-    # Check validation of arguments
-    if not 0 <= args.ot11_num <= MAX_OT11_NUM:
-        raise ValueError(f'The number of FTDs should be between 0 and {MAX_OT11_NUM}')
-
-    if not 0 <= args.sniffer_num <= MAX_SNIFFER_NUM:
-        raise ValueError(f'The number of FTDs should be between 0 and {MAX_SNIFFER_NUM}')
-
-    if args.ot11_num == args.sniffer_num == 0:
-        raise ValueError('At least one device is required')
-
-    # Get the local IP address on the specified interface
-    addr = get_ipaddr(args.ifname)
-
-    s = init_socket(args.ifname, GROUP, PORT)
-
-    logging.info('Advertising on interface %s group %s ...', args.ifname, GROUP)
-
-    # Loop, printing any data we receive
-    while True:
-        data, src = s.recvfrom(64)
-
-        if data == b'BBR':
-            logging.info('Received OpenThread simulation query, advertising')
-            advertise_ftd(s, src, ven='OpenThread_Sim', ver='4', add=addr, por=22, number=args.ot11_num)
-
-        elif data == b'Sniffer':
-            logging.info('Received sniffer simulation query, advertising')
-            advertise_sniffer(s, src, add=addr, number=args.sniffer_num)
-
-        else:
-            logging.warning('Received %r, but ignored', data)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/tools/harness-simulation/posix/install.sh b/tools/harness-simulation/posix/install.sh
new file mode 100755
index 0000000..aedba39
--- /dev/null
+++ b/tools/harness-simulation/posix/install.sh
@@ -0,0 +1,149 @@
+#!/bin/bash
+#
+# Copyright (c) 2022, 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 -euxo pipefail
+
+POSIX_DIR="$(cd "$(dirname "$0")" && pwd)"
+OT_DIR="${POSIX_DIR}/../../.."
+ETC_DIR="${POSIX_DIR}/etc"
+SNIFFER_DIR="${POSIX_DIR}/sniffer_sim"
+
+PACKAGES=(
+    "docker.io"
+    "git"
+    "jq"
+    "socat"
+    "tshark"
+)
+
+sudo apt install -y "${PACKAGES[@]}"
+
+pip3 install -r "${POSIX_DIR}/requirements.txt"
+python3 -m grpc_tools.protoc -I"${SNIFFER_DIR}" --python_out="${SNIFFER_DIR}" --grpc_python_out="${SNIFFER_DIR}" proto/sniffer.proto
+
+CONFIG_NAME=${1:-"${POSIX_DIR}/config.yml"}
+# convert YAML to JSON
+CONFIG=$(python3 -c 'import json, sys, yaml; print(json.dumps(yaml.safe_load(open(sys.argv[1]))))' "$CONFIG_NAME")
+
+MAX_NETWORK_SIZE=$(jq -r '.ot_build.max_number' <<<"$CONFIG")
+
+build_ot()
+{
+    # SC2155: Declare and assign separately to avoid masking return values
+    local target build_dir cflags version options
+    target="ot-cli-ftd"
+    build_dir=$(jq -r '.subpath' <<<"$1")
+    cflags=$(jq -r '.cflags | join(" ")' <<<"$1")
+    version=$(jq -r '.version' <<<"$1")
+    options=$(jq -r '.options | join(" ")' <<<"$1")
+    # Intended splitting of options
+    read -ra options <<<"$options"
+
+    (
+        cd "$OT_DIR"
+
+        OT_CMAKE_NINJA_TARGET="$target" \
+            OT_CMAKE_BUILD_DIR="$build_dir" \
+            CFLAGS="$cflags" \
+            CXXFLAGS="$cflags" \
+            script/cmake-build \
+            simulation \
+            "${options[@]}" \
+            -DOT_THREAD_VERSION="$version" \
+            -DOT_SIMULATION_MAX_NETWORK_SIZE="$MAX_NETWORK_SIZE"
+    )
+}
+
+build_otbr()
+{
+    # SC2155: Declare and assign separately to avoid masking return values
+    local target build_dir version rcp_options
+    target="ot-rcp"
+    build_dir=$(jq -r '.rcp_subpath' <<<"$1")
+    version=$(jq -r '.version' <<<"$1")
+    rcp_options=$(jq -r '.rcp_options | join(" ")' <<<"$1")
+    # Intended splitting of rcp_options
+    read -ra rcp_options <<<"$rcp_options"
+
+    (
+        cd "$OT_DIR"
+
+        OT_CMAKE_NINJA_TARGET="$target" \
+            OT_CMAKE_BUILD_DIR="$build_dir" \
+            script/cmake-build \
+            simulation \
+            "${rcp_options[@]}" \
+            -DOT_THREAD_VERSION="$version" \
+            -DOT_SIMULATION_MAX_NETWORK_SIZE="$MAX_NETWORK_SIZE"
+    )
+
+    # SC2155: Declare and assign separately to avoid masking return values
+    local otbr_docker_image build_args options
+    otbr_docker_image=$(jq -r '.docker_image' <<<"$1")
+    build_args=$(jq -r '.build_args | map("--build-arg " + .) | join(" ")' <<<"$1")
+    # Intended splitting of build_args
+    read -ra build_args <<<"$build_args"
+    options=$(jq -r '.options | join(" ")' <<<"$1")
+
+    local otbr_options=(
+        "$options"
+        "-DOT_THREAD_VERSION=$version"
+        "-DOT_SIMULATION_MAX_NETWORK_SIZE=$MAX_NETWORK_SIZE"
+    )
+
+    docker build . \
+        -t "${otbr_docker_image}" \
+        -f "${ETC_DIR}/Dockerfile" \
+        "${build_args[@]}" \
+        --build-arg OTBR_OPTIONS="${otbr_options[*]}"
+}
+
+for item in $(jq -c '.ot_build.ot | .[]' <<<"$CONFIG"); do
+    build_ot "$item"
+done
+
+git clone https://github.com/openthread/ot-br-posix.git --recurse-submodules --shallow-submodules --depth=1
+(
+    cd ot-br-posix
+    # Use system V `service` command instead
+    mkdir -p root/etc/init.d
+    cp "${ETC_DIR}/commissionerd" root/etc/init.d/commissionerd
+    sudo chown root:root root/etc/init.d/commissionerd
+    sudo chmod +x root/etc/init.d/commissionerd
+
+    cp "${ETC_DIR}/server.patch" script/server.patch
+    patch script/server script/server.patch
+    mkdir -p root/tmp
+    cp "${ETC_DIR}/requirements.txt" root/tmp/requirements.txt
+
+    for item in $(jq -c '.ot_build.otbr | .[]' <<<"$CONFIG"); do
+        build_otbr "$item"
+    done
+)
+rm -rf ot-br-posix
diff --git a/tools/harness-simulation/posix/launch_testbed.py b/tools/harness-simulation/posix/launch_testbed.py
new file mode 100755
index 0000000..5ca2537
--- /dev/null
+++ b/tools/harness-simulation/posix/launch_testbed.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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.
+#
+
+import argparse
+import ctypes
+import ctypes.util
+import ipaddress
+import json
+import logging
+import os
+import signal
+import socket
+import struct
+import subprocess
+import sys
+from typing import Iterable
+import yaml
+
+from otbr_sim import otbr_docker
+
+GROUP = 'ff02::114'
+PORT = 12345
+
+
+def if_nametoindex(ifname: str) -> int:
+    libc = ctypes.CDLL(ctypes.util.find_library('c'))
+    ret = libc.if_nametoindex(ifname.encode('ascii'))
+    if not ret:
+        raise RuntimeError('Invalid interface name')
+    return ret
+
+
+def get_ipaddr(ifname: str) -> str:
+    for line in os.popen(f'ip addr list dev {ifname} | grep inet | grep global'):
+        addr = line.strip().split()[1]
+        return addr.split('/')[0]
+    raise RuntimeError(f'No IP address on dev {ifname}')
+
+
+def init_socket(ifname: str, group: str, port: int) -> socket.socket:
+    # Look up multicast group address in name server and find out IP version
+    addrinfo = socket.getaddrinfo(group, None)[0]
+    assert addrinfo[0] == socket.AF_INET6
+
+    # Create a socket
+    s = socket.socket(addrinfo[0], socket.SOCK_DGRAM)
+    s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, (ifname + '\0').encode('ascii'))
+
+    # Bind it to the port
+    s.bind((group, port))
+
+    group_bin = socket.inet_pton(addrinfo[0], addrinfo[4][0])
+    # Join group
+    interface_index = if_nametoindex(ifname)
+    mreq = group_bin + struct.pack('@I', interface_index)
+    s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
+
+    return s
+
+
+def _advertise(s: socket.socket, dst, info):
+    logging.info('Advertise: %r', info)
+    s.sendto(json.dumps(info).encode('utf-8'), dst)
+
+
+def advertise_devices(s: socket.socket, dst, ven: str, add: str, nodeids: Iterable[int], tag: str):
+    for nodeid in nodeids:
+        info = {
+            'ven': ven,
+            'mod': 'OpenThread',
+            'ver': '4',
+            'add': f'{tag}_{nodeid}@{add}',
+            'por': 22,
+        }
+        _advertise(s, dst, info)
+
+
+def advertise_sniffers(s: socket.socket, dst, add: str, ports: Iterable[int]):
+    for port in ports:
+        info = {
+            'add': add,
+            'por': port,
+        }
+        _advertise(s, dst, info)
+
+
+def start_sniffer(addr: str, port: int, ot_path: str, max_nodes_num: int) -> subprocess.Popen:
+    if isinstance(ipaddress.ip_address(addr), ipaddress.IPv6Address):
+        server = f'[{addr}]:{port}'
+    else:
+        server = f'{addr}:{port}'
+
+    cmd = [
+        'python3',
+        os.path.join(ot_path, 'tools/harness-simulation/posix/sniffer_sim/sniffer.py'),
+        '--grpc-server',
+        server,
+        '--max-nodes-num',
+        str(max_nodes_num),
+    ]
+    logging.info('Executing command:  %s', ' '.join(cmd))
+    return subprocess.Popen(cmd)
+
+
+def main():
+    logging.basicConfig(level=logging.INFO)
+
+    # Parse arguments
+    parser = argparse.ArgumentParser()
+    parser.add_argument('-c',
+                        '--config',
+                        dest='config',
+                        type=str,
+                        required=True,
+                        help='the path of the configuration JSON file')
+    args = parser.parse_args()
+    with open(args.config, 'rt') as f:
+        config = yaml.safe_load(f)
+
+    ot_path = config['ot_path']
+    ot_build = config['ot_build']
+    max_nodes_num = ot_build['max_number']
+    # No test case requires more than 2 sniffers
+    MAX_SNIFFER_NUM = 2
+
+    ot_devices = [(item['tag'], item['number']) for item in ot_build['ot']]
+    otbr_devices = [(item['tag'], item['number']) for item in ot_build['otbr']]
+    ot_nodes_num = sum(x[1] for x in ot_devices)
+    otbr_nodes_num = sum(x[1] for x in otbr_devices)
+    nodes_num = ot_nodes_num + otbr_nodes_num
+    sniffer_num = config['sniffer']['number']
+
+    # Check validation of numbers
+    if not all(0 <= x[1] <= max_nodes_num for x in ot_devices):
+        raise ValueError(f'The number of devices of each OT version should be between 0 and {max_nodes_num}')
+
+    if not all(0 <= x[1] <= max_nodes_num for x in otbr_devices):
+        raise ValueError(f'The number of devices of each OTBR version should be between 0 and {max_nodes_num}')
+
+    if not 1 <= nodes_num <= max_nodes_num:
+        raise ValueError(f'The number of devices should be between 1 and {max_nodes_num}')
+
+    if not 1 <= sniffer_num <= MAX_SNIFFER_NUM:
+        raise ValueError(f'The number of sniffers should be between 1 and {MAX_SNIFFER_NUM}')
+
+    # Get the local IP address on the specified interface
+    ifname = config['discovery_ifname']
+    addr = get_ipaddr(ifname)
+
+    # Start the sniffer
+    sniffer_server_port_base = config['sniffer']['server_port_base']
+    sniffer_procs = []
+    for i in range(sniffer_num):
+        sniffer_procs.append(start_sniffer(addr, i + sniffer_server_port_base, ot_path, max_nodes_num))
+
+    # OTBR firewall scripts create rules inside the Docker container
+    # Run modprobe to load the kernel modules for iptables
+    subprocess.run(['sudo', 'modprobe', 'ip6table_filter'])
+    # Start the BRs
+    otbr_dockers = []
+    nodeid = ot_nodes_num
+    for item in ot_build['otbr']:
+        tag = item['tag']
+        ot_rcp_path = os.path.join(ot_path, item['rcp_subpath'], 'examples/apps/ncp/ot-rcp')
+        docker_image = item['docker_image']
+        for _ in range(item['number']):
+            nodeid += 1
+            otbr_dockers.append(
+                otbr_docker.OtbrDocker(nodeid=nodeid,
+                                       ot_path=ot_path,
+                                       ot_rcp_path=ot_rcp_path,
+                                       docker_image=docker_image,
+                                       docker_name=f'{tag}_{nodeid}'))
+
+    s = init_socket(ifname, GROUP, PORT)
+
+    logging.info('Advertising on interface %s group %s ...', ifname, GROUP)
+
+    # Terminate all sniffer simulation server processes and then exit
+    def exit_handler(signum, context):
+        # Return code is non-zero if any return code of the processes is non-zero
+        ret = 0
+        for sniffer_proc in sniffer_procs:
+            sniffer_proc.terminate()
+            ret = max(ret, sniffer_proc.wait())
+
+        for otbr in otbr_dockers:
+            otbr.close()
+
+        sys.exit(ret)
+
+    signal.signal(signal.SIGINT, exit_handler)
+    signal.signal(signal.SIGTERM, exit_handler)
+
+    # Loop, printing any data we receive
+    while True:
+        data, src = s.recvfrom(64)
+
+        if data == b'BBR':
+            logging.info('Received OpenThread simulation query, advertising')
+
+            nodeid = 1
+            for ven, devices in [('OpenThread_Sim', ot_devices), ('OpenThread_BR_Sim', otbr_devices)]:
+                for tag, number in devices:
+                    advertise_devices(s, src, ven=ven, add=addr, nodeids=range(nodeid, nodeid + number), tag=tag)
+                    nodeid += number
+
+        elif data == b'Sniffer':
+            logging.info('Received sniffer simulation query, advertising')
+            advertise_sniffers(s,
+                               src,
+                               add=addr,
+                               ports=range(sniffer_server_port_base, sniffer_server_port_base + sniffer_num))
+
+        else:
+            logging.warning('Received %r, but ignored', data)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tools/harness-simulation/posix/otbr_sim/otbr_docker.py b/tools/harness-simulation/posix/otbr_sim/otbr_docker.py
new file mode 100644
index 0000000..cc7f73b
--- /dev/null
+++ b/tools/harness-simulation/posix/otbr_sim/otbr_docker.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2022, 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.
+#
+
+import logging
+import os
+import re
+import subprocess
+import time
+
+
+class OtbrDocker:
+    device_pattern = re.compile('(?<=PTY is )/dev/.+$')
+
+    def __init__(self, nodeid: int, ot_path: str, ot_rcp_path: str, docker_image: str, docker_name: str):
+        self.nodeid = nodeid
+        self.ot_path = ot_path
+        self.ot_rcp_path = ot_rcp_path
+        self.docker_image = docker_image
+        self.docker_name = docker_name
+
+        self.logger = logging.getLogger('otbr_docker.OtbrDocker')
+        self.logger.setLevel(logging.INFO)
+
+        self._socat_proc = None
+        self._ot_rcp_proc = None
+
+        self._rcp_device_pty = None
+        self._rcp_device = None
+
+        self._launch()
+
+    def __repr__(self) -> str:
+        return f'OTBR<{self.nodeid}>'
+
+    def _launch(self):
+        self.logger.info('Launching %r ...', self)
+        self._launch_socat()
+        self._launch_ot_rcp()
+        self._launch_docker()
+        self.logger.info('Launched %r successfully', self)
+
+    def close(self):
+        self.logger.info('Shutting down %r ...', self)
+        self._shutdown_docker()
+        self._shutdown_ot_rcp()
+        self._shutdown_socat()
+        self.logger.info('Shut down %r successfully', self)
+
+    def _launch_socat(self):
+        self._socat_proc = subprocess.Popen(['socat', '-d', '-d', 'pty,raw,echo=0', 'pty,raw,echo=0'],
+                                            stderr=subprocess.PIPE,
+                                            stdin=subprocess.DEVNULL,
+                                            stdout=subprocess.DEVNULL)
+
+        line = self._socat_proc.stderr.readline().decode('ascii').strip()
+        self._rcp_device_pty = self.device_pattern.findall(line)[0]
+        line = self._socat_proc.stderr.readline().decode('ascii').strip()
+        self._rcp_device = self.device_pattern.findall(line)[0]
+        self.logger.info(f"socat running: device PTY: {self._rcp_device_pty}, device: {self._rcp_device}")
+
+    def _shutdown_socat(self):
+        if self._socat_proc is None:
+            return
+
+        self._socat_proc.stderr.close()
+        self._socat_proc.terminate()
+        self._socat_proc.wait()
+        self._socat_proc = None
+
+        self._rcp_device_pty = None
+        self._rcp_device = None
+
+    def _launch_ot_rcp(self):
+        self._ot_rcp_proc = subprocess.Popen(
+            f'{self.ot_rcp_path} {self.nodeid} > {self._rcp_device_pty} < {self._rcp_device_pty}',
+            shell=True,
+            stdin=subprocess.DEVNULL,
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL)
+        try:
+            self._ot_rcp_proc.wait(1)
+        except subprocess.TimeoutExpired:
+            # We expect ot-rcp not to quit in 1 second.
+            pass
+        else:
+            raise Exception(f"ot-rcp {self.nodeid} exited unexpectedly!")
+
+    def _shutdown_ot_rcp(self):
+        if self._ot_rcp_proc is None:
+            return
+
+        self._ot_rcp_proc.terminate()
+        self._ot_rcp_proc.wait()
+        self._ot_rcp_proc = None
+
+    def _launch_docker(self):
+        local_cmd_path = f'/tmp/{self.docker_name}'
+        os.makedirs(local_cmd_path, exist_ok=True)
+
+        cmd = [
+            'docker',
+            'run',
+            '--rm',
+            '--name',
+            self.docker_name,
+            '-d',
+            '--sysctl',
+            'net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1',
+            '--privileged',
+            '-v',
+            f'{self._rcp_device}:/dev/ttyUSB0',
+            '-v',
+            f'{self.ot_path.rstrip("/")}:/home/pi/repo/openthread',
+            self.docker_image,
+        ]
+        self.logger.info('Launching docker:  %s', ' '.join(cmd))
+        launch_proc = subprocess.Popen(cmd,
+                                       stdin=subprocess.DEVNULL,
+                                       stdout=subprocess.DEVNULL,
+                                       stderr=subprocess.DEVNULL)
+
+        launch_docker_deadline = time.time() + 60
+        launch_ok = False
+        time.sleep(5)
+
+        while time.time() < launch_docker_deadline:
+            try:
+                subprocess.check_call(['docker', 'exec', self.docker_name, 'ot-ctl', 'state'],
+                                      stdin=subprocess.DEVNULL,
+                                      stdout=subprocess.DEVNULL,
+                                      stderr=subprocess.DEVNULL)
+                launch_ok = True
+                logging.info("OTBR Docker %s is ready!", self.docker_name)
+                break
+            except subprocess.CalledProcessError:
+                time.sleep(5)
+                continue
+
+        if not launch_ok:
+            raise RuntimeError('Cannot start OTBR Docker %s!' % self.docker_name)
+        launch_proc.wait()
+
+    def _shutdown_docker(self):
+        subprocess.run(['docker', 'stop', self.docker_name])
diff --git a/tools/harness-simulation/posix/requirements.txt b/tools/harness-simulation/posix/requirements.txt
new file mode 100644
index 0000000..21ed00b
--- /dev/null
+++ b/tools/harness-simulation/posix/requirements.txt
@@ -0,0 +1,3 @@
+grpcio
+grpcio-tools
+PyYAML
diff --git a/tools/harness-simulation/posix/sniffer_sim/pcap_codec.py b/tools/harness-simulation/posix/sniffer_sim/pcap_codec.py
index ff25356..3817c70 100644
--- a/tools/harness-simulation/posix/sniffer_sim/pcap_codec.py
+++ b/tools/harness-simulation/posix/sniffer_sim/pcap_codec.py
@@ -42,16 +42,20 @@
 class PcapCodec(object):
     """ Utility class for .pcap formatters. """
 
-    def __init__(self, filename, channel):
+    def __init__(self, channel, filename):
         self._dlt = DLT_IEEE802_15_4_WITHFCS
-        if not filename.endswith('.pcap'):
-            raise ValueError('Filename should end with .pcap')
-        self._pcap_file = open(filename, 'wb')
-        self._pcap_file.write(self.encode_header())
         self._channel = channel
 
-    def encode_header(self):
+        self._pcap_writer = open(filename, 'wb')
+        self._write(self._encode_header())
+
+    def _write(self, content):
+        self._pcap_writer.write(content)
+        self._pcap_writer.flush()
+
+    def _encode_header(self):
         """ Return a pcap file header. """
+
         return struct.pack(
             '<LHHLLLL',
             PCAP_MAGIC_NUMBER,
@@ -63,7 +67,7 @@
             self._dlt,
         )
 
-    def encode_frame(self, frame, sec, usec):
+    def _encode_frame(self, frame, sec, usec):
         """ Return a pcap encapsulation of the given frame. """
 
         # Ignore the first byte storing channel.
@@ -76,6 +80,7 @@
 
     def _get_timestamp(self):
         """ Return the internal timestamp. """
+
         timestamp = time.time()
         timestamp_sec = int(timestamp)
         timestamp_usec = int((timestamp - timestamp_sec) * 1000000)
@@ -89,11 +94,7 @@
             return
 
         timestamp = self._get_timestamp()
-        pkt = self.encode_frame(frame, *timestamp)
-        self._pcap_file.write(pkt)
-        self._pcap_file.flush()
+        self._write(self._encode_frame(frame, *timestamp))
 
     def close(self):
-        """ Close the pcap file. """
-
-        self._pcap_file.close()
+        self._pcap_writer.close()
diff --git a/tools/harness-simulation/posix/sniffer_sim/proto/sniffer.proto b/tools/harness-simulation/posix/sniffer_sim/proto/sniffer.proto
new file mode 100644
index 0000000..9d843f0
--- /dev/null
+++ b/tools/harness-simulation/posix/sniffer_sim/proto/sniffer.proto
@@ -0,0 +1,67 @@
+syntax = "proto3";
+
+package sniffer;
+
+// Sniffer simulation
+service Sniffer {
+    // Start the sniffer
+    rpc Start(StartRequest) returns (StartResponse) {}
+
+    // Transfer the capture file
+    rpc TransferPcapng(TransferPcapngRequest) returns (stream TransferPcapngResponse) {}
+
+    // Let the sniffer sniff these nodes only
+    rpc FilterNodes(FilterNodesRequest) returns (FilterNodesResponse) {}
+
+    // Stop the sniffer
+    rpc Stop(StopRequest) returns (StopResponse) {}
+}
+
+// Possible Status which the RPCs may return
+enum Status {
+    // Default value which is unused
+    STATUS_UNSPECIFIED = 0;
+
+    // Everything goes well
+    OK = 1;
+
+    // Unable to run the specified RPC currently
+    OPERATION_ERROR = 2;
+
+    // The parameters passed to the RPC is erroneous
+    VALUE_ERROR = 3;
+}
+
+message StartRequest {
+    // Specify the channel that the sniffer is going to sniff
+    int32 channel = 1;
+
+    // Specify whether to include Ethernet packets between OTBR and infra
+    bool includeEthernet = 2;
+}
+
+message StartResponse {
+    Status status = 1;
+}
+
+message TransferPcapngRequest {
+}
+
+message TransferPcapngResponse {
+    bytes content = 1;
+}
+
+message FilterNodesRequest {
+    repeated int32 nodeids = 1;
+}
+
+message FilterNodesResponse {
+    Status status = 1;
+}
+
+message StopRequest {
+}
+
+message StopResponse {
+    Status status = 1;
+}
diff --git a/tools/harness-simulation/posix/sniffer_sim/sniffer.py b/tools/harness-simulation/posix/sniffer_sim/sniffer.py
index 0ef6899..f352131 100644
--- a/tools/harness-simulation/posix/sniffer_sim/sniffer.py
+++ b/tools/harness-simulation/posix/sniffer_sim/sniffer.py
@@ -28,104 +28,228 @@
 #
 
 import argparse
+from concurrent import futures
+import enum
+import fcntl
+import grpc
 import logging
+import os
 import signal
-import time
-import pcap_codec
-import sys
+import socket
+import subprocess
+import tempfile
 import threading
+import time
 
+import pcap_codec
+from proto import sniffer_pb2
+from proto import sniffer_pb2_grpc
 import sniffer_transport
 
 
-class Sniffer:
-    """ Class representing the Sniffing node, whose main task is listening.
-    """
+class CaptureState(enum.Flag):
+    NONE = 0
+    THREAD = enum.auto()
+    ETHERNET = enum.auto()
 
-    logger = logging.getLogger('sniffer.Sniffer')
+
+class SnifferServicer(sniffer_pb2_grpc.Sniffer):
+    """ Class representing the Sniffing node, whose main task is listening. """
+
+    logger = logging.getLogger('sniffer.SnifferServicer')
 
     RECV_BUFFER_SIZE = 4096
+    TIMEOUT = 0.1
 
-    def __init__(self, filename, channel):
-        self._pcap = pcap_codec.PcapCodec(filename, channel)
+    def _reset(self):
+        self._state = CaptureState.NONE
+        self._pcap = None
+        self._denied_nodeids = None
+        self._transport = None
+        self._thread = None
+        self._thread_alive.clear()
+        self._file_sync_done.clear()
+        self._tshark_proc = None
+
+    def __init__(self, max_nodes_num):
+        self._max_nodes_num = max_nodes_num
+        self._thread_alive = threading.Event()
+        self._file_sync_done = threading.Event()
+        self._nodeids_mutex = threading.Lock()  # for `self._denied_nodeids`
+        self._reset()
+
+    def Start(self, request, context):
+        """ Start sniffing. """
+
+        self.logger.debug('call Start')
+
+        # Validate and change the state
+        if self._state != CaptureState.NONE:
+            return sniffer_pb2.StartResponse(status=sniffer_pb2.OPERATION_ERROR)
+        self._state = CaptureState.THREAD
+
+        # Create a temporary named pipe
+        tempdir = tempfile.mkdtemp()
+        fifo_name = os.path.join(tempdir, 'pcap.fifo')
+        os.mkfifo(fifo_name)
+
+        cmd = ['tshark', '-i', fifo_name]
+        if request.includeEthernet:
+            self._state |= CaptureState.ETHERNET
+            cmd += ['-i', 'docker0']
+        cmd += ['-w', '-', '-q', 'not ip and not tcp and not arp and not ether proto 0x8899']
+
+        self.logger.debug('Running command:  %s', ' '.join(cmd))
+        self._tshark_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+        self._set_nonblocking(self._tshark_proc.stdout.fileno())
+
+        # Construct pcap codec after initiating tshark to avoid blocking
+        self._pcap = pcap_codec.PcapCodec(request.channel, fifo_name)
+
+        # Sniffer all nodes in default, i.e. there is no RF enclosure
+        self._denied_nodeids = set()
 
         # Create transport
         transport_factory = sniffer_transport.SnifferTransportFactory()
         self._transport = transport_factory.create_transport()
 
-        self._thread = None
-        self._thread_alive = threading.Event()
-        self._thread_alive.clear()
+        # Start the sniffer main loop thread
+        self._thread = threading.Thread(target=self._sniffer_main_loop)
+        self._thread.setDaemon(True)
+        self._transport.open()
+        self._thread_alive.set()
+        self._thread.start()
+
+        return sniffer_pb2.StartResponse(status=sniffer_pb2.OK)
 
     def _sniffer_main_loop(self):
         """ Sniffer main loop. """
 
-        self.logger.debug('Sniffer started.')
-
         while self._thread_alive.is_set():
-            data, nodeid = self._transport.recv(self.RECV_BUFFER_SIZE)
-            self._pcap.append(data)
+            try:
+                data, nodeid = self._transport.recv(self.RECV_BUFFER_SIZE, self.TIMEOUT)
+            except socket.timeout:
+                continue
 
-        self.logger.debug('Sniffer stopped.')
+            with self._nodeids_mutex:
+                denied_nodeids = self._denied_nodeids
 
-    def start(self):
-        """ Start sniffing. """
+            # Equivalent to RF enclosure
+            if nodeid not in denied_nodeids:
+                self._pcap.append(data)
 
-        self._thread = threading.Thread(target=self._sniffer_main_loop)
-        self._thread.daemon = True
+    def TransferPcapng(self, request, context):
+        """ Transfer the capture file. """
 
-        self._transport.open()
+        # Validate the state
+        if self._state == CaptureState.NONE:
+            return sniffer_pb2.FilterNodesResponse(status=sniffer_pb2.OPERATION_ERROR)
 
-        self._thread_alive.set()
-        self._thread.start()
+        # Synchronize the capture file
+        while True:
+            content = self._tshark_proc.stdout.read()
+            if content is None:
+                # Currently no captured packets
+                time.sleep(self.TIMEOUT)
+            elif content == b'':
+                # Reach EOF when tshark terminates
+                break
+            else:
+                # Forward the captured packets
+                yield sniffer_pb2.TransferPcapngResponse(content=content)
 
-    def stop(self):
-        """ Stop sniffing. """
+        self._file_sync_done.set()
+
+    def _set_nonblocking(self, fd):
+        flags = fcntl.fcntl(fd, fcntl.F_GETFL)
+        if flags < 0:
+            raise RuntimeError('fcntl(F_GETFL) failed')
+
+        flags |= os.O_NONBLOCK
+        if fcntl.fcntl(fd, fcntl.F_SETFL, flags) < 0:
+            raise RuntimeError('fcntl(F_SETFL) failed')
+
+    def FilterNodes(self, request, context):
+        """ Only sniffer the specified nodes. """
+
+        self.logger.debug('call FilterNodes')
+
+        # Validate the state
+        if not (self._state & CaptureState.THREAD):
+            return sniffer_pb2.FilterNodesResponse(status=sniffer_pb2.OPERATION_ERROR)
+
+        denied_nodeids = set(request.nodeids)
+        # Validate the node IDs
+        for nodeid in denied_nodeids:
+            if not 1 <= nodeid <= self._max_nodes_num:
+                return sniffer_pb2.FilterNodesResponse(status=sniffer_pb2.VALUE_ERROR)
+
+        with self._nodeids_mutex:
+            self._denied_nodeids = denied_nodeids
+
+        return sniffer_pb2.FilterNodesResponse(status=sniffer_pb2.OK)
+
+    def Stop(self, request, context):
+        """ Stop sniffing, and return the pcap bytes. """
+
+        self.logger.debug('call Stop')
+
+        # Validate and change the state
+        if not (self._state & CaptureState.THREAD):
+            return sniffer_pb2.StopResponse(status=sniffer_pb2.OPERATION_ERROR)
+        self._state = CaptureState.NONE
 
         self._thread_alive.clear()
-
+        self._thread.join()
         self._transport.close()
-
-        self._thread.join(timeout=1)
-        self._thread = None
-
-    def close(self):
-        """ Close the pcap file. """
-
         self._pcap.close()
 
+        self._tshark_proc.terminate()
+        self._file_sync_done.wait()
+        # `self._tshark_proc` becomes None after the next statement
+        self._tshark_proc.wait()
+
+        self._reset()
+
+        return sniffer_pb2.StopResponse(status=sniffer_pb2.OK)
+
+
+def serve(address_port, max_nodes_num):
+    # One worker is used for `Start`, `FilterNodes` and `Stop`
+    # The other worker is used for `TransferPcapng`, which will be kept running by the client in a background thread
+    server = grpc.server(futures.ThreadPoolExecutor(max_workers=2))
+    sniffer_pb2_grpc.add_SnifferServicer_to_server(SnifferServicer(max_nodes_num), server)
+    # add_secure_port requires a web domain
+    server.add_insecure_port(address_port)
+    logging.info('server starts on %s', address_port)
+    server.start()
+
+    def exit_handler(signum, context):
+        server.stop(1)
+
+    signal.signal(signal.SIGINT, exit_handler)
+    signal.signal(signal.SIGTERM, exit_handler)
+
+    server.wait_for_termination()
+
 
 def run_sniffer():
+    logging.basicConfig(level=logging.INFO)
+
     parser = argparse.ArgumentParser()
-    parser.add_argument('-o',
-                        '--output',
-                        dest='output',
+    parser.add_argument('--grpc-server',
+                        dest='grpc_server',
                         type=str,
                         required=True,
-                        help='the path of the output .pcap file')
-    parser.add_argument('-c',
-                        '--channel',
-                        dest='channel',
+                        help='the address of the sniffer server')
+    parser.add_argument('--max-nodes-num',
+                        dest='max_nodes_num',
                         type=int,
                         required=True,
-                        help='the channel which is sniffered')
+                        help='the maximum number of nodes')
     args = parser.parse_args()
 
-    sniffer = Sniffer(args.output, args.channel)
-    sniffer.start()
-
-    def atexit(signum, frame):
-        sniffer.stop()
-        sniffer.close()
-        sys.exit(0)
-
-    signal.signal(signal.SIGTERM, atexit)
-
-    while sniffer._thread_alive.is_set():
-        time.sleep(0.5)
-
-    sniffer.stop()
-    sniffer.close()
+    serve(args.grpc_server, args.max_nodes_num)
 
 
 if __name__ == '__main__':
diff --git a/tools/harness-simulation/posix/sniffer_sim/sniffer_transport.py b/tools/harness-simulation/posix/sniffer_sim/sniffer_transport.py
index 62cf179..721f4d2 100644
--- a/tools/harness-simulation/posix/sniffer_sim/sniffer_transport.py
+++ b/tools/harness-simulation/posix/sniffer_sim/sniffer_transport.py
@@ -71,17 +71,21 @@
         """
         raise NotImplementedError
 
-    def recv(self, bufsize):
+    def recv(self, bufsize, timeout):
         """ Receive data sent by other node.
 
         Args:
             bufsize (int): size of buffer for incoming data.
+            timeout (float | None): socket timeout.
 
         Returns:
             A tuple contains data and node id.
 
             For example:
             (bytearray([0x00, 0x01...], 1)
+
+        Raises:
+            socket.timeout: when receiving the packets times out.
         """
         raise NotImplementedError
 
@@ -100,12 +104,6 @@
     def __init__(self):
         self._socket = None
 
-    def __del__(self):
-        if not self.is_opened:
-            return
-
-        self.close()
-
     def _nodeid_to_port(self, nodeid: int):
         return self.BASE_PORT + (self.PORT_OFFSET * (self.MAX_NETWORK_SIZE + 1)) + nodeid
 
@@ -145,7 +143,8 @@
 
         return self._socket.sendto(data, address)
 
-    def recv(self, bufsize):
+    def recv(self, bufsize, timeout):
+        self._socket.settimeout(timeout)
         data, address = self._socket.recvfrom(bufsize)
 
         nodeid = self._port_to_nodeid(address[1])
diff --git a/tools/harness-thci/OpenThread.py b/tools/harness-thci/OpenThread.py
index 86bcb46..2410bc4 100644
--- a/tools/harness-thci/OpenThread.py
+++ b/tools/harness-thci/OpenThread.py
@@ -61,7 +61,7 @@
     PlatformDiagnosticPacket_Direction,
     PlatformDiagnosticPacket_Type,
 )
-from GRLLibs.UtilityModules.enums import DevCapb
+from GRLLibs.UtilityModules.enums import DevCapb, TestMode
 
 from IThci import IThci
 import commissioner
@@ -199,6 +199,8 @@
 
     IsBorderRouter = False
     IsHost = False
+    IsBeingTestedAsCommercialBBR = False
+    IsReference20200818 = False
 
     externalCommissioner = None
     _update_router_status = False
@@ -253,13 +255,29 @@
             line str: data send to device
         """
 
-    @abstractmethod
+    # Override the following empty methods in the dervied classes when needed
     def _onCommissionStart(self):
-        """Called when commissioning starts."""
+        """Called when commissioning starts"""
 
-    @abstractmethod
     def _onCommissionStop(self):
-        """Called when commissioning stops."""
+        """Called when commissioning stops"""
+
+    def _deviceBeforeReset(self):
+        """Called before the device resets"""
+
+    def _deviceAfterReset(self):
+        """Called after the device resets"""
+
+    def _restartAgentService(self):
+        """Restart the agent service"""
+
+    def _beforeRegisterMulticast(self, sAddr, timeout):
+        """Called before the ipv6 address being subscribed in interface
+
+        Args:
+            sAddr   : str : Multicast address to be subscribed and notified OTA
+            timeout : int : The allowed maximal time to end normally
+        """
 
     def __sendCommand(self, cmd, expectEcho=True):
         cmd = self._cmdPrefix + cmd
@@ -383,31 +401,13 @@
     @API
     def intialize(self, params):
         """initialize the serial port with baudrate, timeout parameters"""
-        self.port = params.get('SerialPort', '')
-        # params example: {'EUI': 1616240311388864514L, 'SerialBaudRate': None, 'TelnetIP': '192.168.8.181', 'SerialPort': None, 'Param7': None, 'Param6': None, 'Param5': 'ip', 'TelnetPort': '22', 'Param9': None, 'Param8': None}
-
-        try:
-
-            ipaddress.ip_address(self.port)
-            # handle TestHarness Discovery Protocol
-            self.connectType = 'ip'
-            self.telnetIp = self.port
-            self.telnetPort = 22
-            self.telnetUsername = 'pi' if params.get('Param6') is None else params.get('Param6')
-            self.telnetPassword = 'raspberry' if params.get('Param7') is None else params.get('Param7')
-        except ValueError:
-            self.connectType = (params.get('Param5') or 'usb').lower()
-            self.telnetIp = params.get('TelnetIP')
-            self.telnetPort = int(params.get('TelnetPort')) if params.get('TelnetPort') else 22
-            # username for SSH
-            self.telnetUsername = 'pi' if params.get('Param6') is None else params.get('Param6')
-            # password for SSH
-            self.telnetPassword = 'raspberry' if params.get('Param7') is None else params.get('Param7')
-
         self.mac = params.get('EUI')
         self.backboneNetif = params.get('Param8') or 'eth0'
         self.extraParams = self.__parseExtraParams(params.get('Param9'))
 
+        # Potentially changes `self.extraParams`
+        self._parseConnectionParams(params)
+
         self.UIStatusMsg = ''
         self.AutoDUTEnable = False
         self.isPowerDown = False
@@ -442,12 +442,42 @@
                                 self.UIStatusMsg)
             ModuleHelper.WriteIntoDebugLogger('Err: OpenThread device Firmware not matching..')
 
+        # Make this class compatible with Thread referenece 20200818
+        self.__detectReference20200818()
+
     def __repr__(self):
         if self.connectType == 'ip':
             return '[%s:%d]' % (self.telnetIp, self.telnetPort)
         else:
             return '[%s]' % self.port
 
+    def _parseConnectionParams(self, params):
+        """Parse parameters related to connection to the device
+
+        Args:
+            params: Arbitrary keyword arguments including 'EUI' and 'SerialPort'
+        """
+        self.port = params.get('SerialPort', '')
+        # params example: {'EUI': 1616240311388864514L, 'SerialBaudRate': None, 'TelnetIP': '192.168.8.181', 'SerialPort': None, 'Param7': None, 'Param6': None, 'Param5': 'ip', 'TelnetPort': '22', 'Param9': None, 'Param8': None}
+        self.log('All parameters: %r', params)
+
+        try:
+            ipaddress.ip_address(self.port)
+            # handle TestHarness Discovery Protocol
+            self.connectType = 'ip'
+            self.telnetIp = self.port
+            self.telnetPort = 22
+            self.telnetUsername = 'pi' if params.get('Param6') is None else params.get('Param6')
+            self.telnetPassword = 'raspberry' if params.get('Param7') is None else params.get('Param7')
+        except ValueError:
+            self.connectType = (params.get('Param5') or 'usb').lower()
+            self.telnetIp = params.get('TelnetIP')
+            self.telnetPort = int(params.get('TelnetPort')) if params.get('TelnetPort') else 22
+            # username for SSH
+            self.telnetUsername = 'pi' if params.get('Param6') is None else params.get('Param6')
+            # password for SSH
+            self.telnetPassword = 'raspberry' if params.get('Param7') is None else params.get('Param7')
+
     @watched
     def __parseExtraParams(self, Param9):
         """
@@ -459,12 +489,14 @@
         - "cmd-start-otbr-agent"   : The command to start otbr-agent (default: systemctl start otbr-agent)
         - "cmd-stop-otbr-agent"    : The command to stop otbr-agent (default: systemctl stop otbr-agent)
         - "cmd-restart-otbr-agent" : The command to restart otbr-agent (default: systemctl restart otbr-agent)
+        - "cmd-restart-radvd"      : The command to restart radvd (default: service radvd restart)
 
         For example, Param9 can be generated as below:
         Param9 = base64.urlsafe_b64encode(json.dumps({
             "cmd-start-otbr-agent": "service otbr-agent start",
             "cmd-stop-otbr-agent": "service otbr-agent stop",
             "cmd-restart-otbr-agent": "service otbr-agent restart",
+            "cmd-restart-radvd": "service radvd stop; service radvd start",
         }))
 
         :param Param9: A JSON string encoded in URL-safe base64 encoding.
@@ -574,11 +606,11 @@
         # reset
         if self.isPowerDown:
             if self._addressfilterMode == 'allowlist':
-                if self.__setAddressfilterMode('allowlist'):
+                if self.__setAddressfilterMode(self.__replaceCommands['allowlist']):
                     for addr in self._addressfilterSet:
                         self.addAllowMAC(addr)
             elif self._addressfilterMode == 'denylist':
-                if self.__setAddressfilterMode('denylist'):
+                if self.__setAddressfilterMode(self.__replaceCommands['denylist']):
                     for addr in self._addressfilterSet:
                         self.addBlockedMAC(addr)
 
@@ -590,9 +622,12 @@
         ]:
             self.__setRouterSelectionJitter(1)
         elif self.deviceRole in [Thread_Device_Role.BR_1, Thread_Device_Role.BR_2]:
+            if ModuleHelper.CurrentRunningTestMode == TestMode.Commercial:
+                # Allow BBR configurations for 1.2 BR_1/BR_2 roles
+                self.IsBeingTestedAsCommercialBBR = True
             self.__setRouterSelectionJitter(1)
 
-        if self.DeviceCapability == OT12BR_CAPBS:
+        if self.IsBeingTestedAsCommercialBBR:
             # Configure default BBR dataset
             self.__configBbrDataset(SeqNum=self.bbrSeqNum,
                                     MlrTimeout=self.bbrMlrTimeout,
@@ -944,14 +979,16 @@
             True: successful to set the Thread network key
             False: fail to set the Thread network key
         """
+        cmdName = self.__replaceCommands['networkkey']
+
         if not isinstance(key, str):
             networkKey = self.__convertLongToHex(key, 32)
-            cmd = 'networkkey %s' % networkKey
-            datasetCmd = 'dataset networkkey %s' % networkKey
+            cmd = '%s %s' % (cmdName, networkKey)
+            datasetCmd = 'dataset %s %s' % (cmdName, networkKey)
         else:
             networkKey = key
-            cmd = 'networkkey %s' % networkKey
-            datasetCmd = 'dataset networkkey %s' % networkKey
+            cmd = '%s %s' % (cmdName, networkKey)
+            datasetCmd = 'dataset %s %s' % (cmdName, networkKey)
 
         self.networkKey = networkKey
         self.hasActiveDatasetToCommit = True
@@ -979,7 +1016,7 @@
             return True
 
         if self._addressfilterMode != 'denylist':
-            if self.__setAddressfilterMode('denylist'):
+            if self.__setAddressfilterMode(self.__replaceCommands['denylist']):
                 self._addressfilterMode = 'denylist'
 
         cmd = 'macfilter addr add %s' % macAddr
@@ -1009,7 +1046,7 @@
             macAddr = self.__convertLongToHex(xEUI)
 
         if self._addressfilterMode != 'allowlist':
-            if self.__setAddressfilterMode('allowlist'):
+            if self.__setAddressfilterMode(self.__replaceCommands['allowlist']):
                 self._addressfilterMode = 'allowlist'
 
         cmd = 'macfilter addr add %s' % macAddr
@@ -1102,10 +1139,7 @@
                 # set ROUTER_DOWNGRADE_THRESHOLD
                 self.__setRouterDowngradeThreshold(33)
         elif eRoleId in (Thread_Device_Role.BR_1, Thread_Device_Role.BR_2):
-            if self.DeviceCapability == OT12BR_CAPBS:
-                print('join as BBR')
-            else:
-                print('join as BR')
+            print('join as BBR')
             mode = 'rdn'
             if self.AutoDUTEnable is False:
                 # set ROUTER_DOWNGRADE_THRESHOLD
@@ -1140,6 +1174,9 @@
         else:
             pass
 
+        if self.IsReference20200818:
+            mode = 's' if mode == '-' else mode + 's'
+
         # set Thread device mode with a given role
         self.__setDeviceMode(mode)
 
@@ -1220,13 +1257,7 @@
     @API
     def powerDown(self):
         """power down the Thread device"""
-        self.__sendCommand('reset', expectEcho=False)
-
-        if not self.IsBorderRouter:
-            self._disconnect()
-            self._connect()
-
-        self.isPowerDown = True
+        self._reset()
 
     @API
     def powerUp(self):
@@ -1238,7 +1269,8 @@
                 self.__setPollPeriod(self.__sedPollPeriod)
             self.__startOpenThread()
 
-    def reset_and_wait_for_connection(self, timeout=3):
+    @watched
+    def _reset(self, timeout=3):
         print("Waiting after reset timeout: {} s".format(timeout))
         start_time = time.time()
         self.__sendCommand('reset', expectEcho=False)
@@ -1257,6 +1289,8 @@
         else:
             raise AssertionError("Could not connect with OT device {} after reset.".format(self))
 
+    def reset_and_wait_for_connection(self, timeout=3):
+        self._reset(timeout=timeout)
         if self.deviceRole == Thread_Device_Role.SED:
             self.__setPollPeriod(self.__sedPollPeriod)
 
@@ -1298,8 +1332,14 @@
             hop_limit: hop limit
 
         """
-        cmd = 'ping %s %s 1 1 %d %d' % (strDestination, str(ilength), hop_limit, timeout)
+        cmd = 'ping %s %s' % (strDestination, str(ilength))
+        if not self.IsReference20200818:
+            cmd += ' 1 1 %d %d' % (hop_limit, timeout)
+
         self.__executeCommand(cmd)
+        if self.IsReference20200818:
+            # wait echo reply
+            self.sleep(6)  # increase delay temporarily (+5s) to remedy TH's delay updates
 
     @API
     def multicast_Ping(self, destination, length=20):
@@ -1353,7 +1393,7 @@
                 self.__executeCommand('state', timeout=0.1)
                 break
             except Exception:
-                self.__restartAgentService()
+                self._restartAgentService()
                 time.sleep(2)
                 self.__sendCommand('factoryreset', expectEcho=False)
                 time.sleep(0.5)
@@ -1364,6 +1404,9 @@
         self.log('factoryreset finished within 10s timeout.')
         self._deviceAfterReset()
 
+        if self.IsBorderRouter:
+            self.__executeCommand('log level 5')
+
     @API
     def removeRouter(self, xRouterId):
         """kickoff router with a given router id from the Thread Network
@@ -1422,8 +1465,9 @@
         # indicate that the channel has been set, in case the channel was set
         # to default when joining network
         self.hasSetChannel = False
+        self.IsBeingTestedAsCommercialBBR = False
         # indicate whether the default domain prefix is used.
-        self.__useDefaultDomainPrefix = (self.DeviceCapability == OT12BR_CAPBS)
+        self.__useDefaultDomainPrefix = True
         self.__isUdpOpened = False
         self.IsHost = False
 
@@ -1432,10 +1476,9 @@
             self.stopListeningToAddrAll()
 
         # BBR dataset
-        if self.DeviceCapability == OT12BR_CAPBS:
-            self.bbrSeqNum = random.randint(0, 126)  # 5.21.4.2
-            self.bbrMlrTimeout = 3600
-            self.bbrReRegDelay = 5
+        self.bbrSeqNum = random.randint(0, 126)  # 5.21.4.2
+        self.bbrMlrTimeout = 3600
+        self.bbrReRegDelay = 5
 
         # initialize device configuration
         self.setMAC(self.mac)
@@ -1560,7 +1603,8 @@
         cmd = 'prefix remove %s/64' % prefixEntry
         if self.__executeCommand(cmd)[-1] == 'Done':
             # send server data ntf to leader
-            return self.__executeCommand('netdata register')[-1] == 'Done'
+            cmd = self.__replaceCommands['netdata register']
+            return self.__executeCommand(cmd)[-1] == 'Done'
         else:
             return False
 
@@ -1642,14 +1686,15 @@
                 return True
             else:
                 # send server data ntf to leader
-                return self.__executeCommand('netdata register')[-1] == 'Done'
+                cmd = self.__replaceCommands['netdata register']
+                return self.__executeCommand(cmd)[-1] == 'Done'
         else:
             return False
 
     @watched
     def getNetworkData(self):
         lines = self.__executeCommand('netdata show')
-        prefixes, routes, services = [], [], []
+        prefixes, routes, services, contexts = [], [], [], []
         classify = None
 
         for line in lines:
@@ -1659,6 +1704,8 @@
                 classify = routes
             elif line == 'Services:':
                 classify = services
+            elif line == 'Contexts:':
+                classify = contexts
             elif line == 'Done':
                 classify = None
             else:
@@ -1668,6 +1715,7 @@
             'Prefixes': prefixes,
             'Routes': routes,
             'Services': services,
+            'Contexts': contexts,
         }
 
     @API
@@ -1794,7 +1842,8 @@
 
         if self.__executeCommand(cmd)[-1] == 'Done':
             # send server data ntf to leader
-            return self.__executeCommand('netdata register')[-1] == 'Done'
+            cmd = self.__replaceCommands['netdata register']
+            return self.__executeCommand(cmd)[-1] == 'Done'
 
     @API
     def getNeighbouringRouters(self):
@@ -1941,7 +1990,8 @@
             True: successful to set the Partition ID
             False: fail to set the Partition ID
         """
-        cmd = 'partitionid preferred %s' % (str(hex(partationId)).rstrip('L'))
+        cmd = self.__replaceCommands['partitionid preferred'] + ' '
+        cmd += str(hex(partationId)).rstrip('L')
         return self.__executeCommand(cmd)[-1] == 'Done'
 
     @API
@@ -2391,7 +2441,7 @@
 
         if len(TLVs) != 0:
             tlvs = ''.join('%02x' % tlv for tlv in TLVs)
-            cmd += ' -x '
+            cmd += ' ' + self.__replaceCommands['-x'] + ' '
             cmd += tlvs
 
         return self.__executeCommand(cmd)[-1] == 'Done'
@@ -2448,7 +2498,7 @@
             cmd += str(sMeshLocalPrefix)
 
         if xMasterKey is not None:
-            cmd += ' networkkey '
+            cmd += ' ' + self.__replaceCommands['networkkey'] + ' '
             key = self.__convertLongToHex(xMasterKey, 32)
 
             cmd += key
@@ -2464,7 +2514,7 @@
         if (sPSKc is not None or listSecurityPolicy is not None or xCommissioningSessionId is not None or
                 xTmfPort is not None or xSteeringData is not None or xBorderRouterLocator is not None or
                 BogusTLV is not None):
-            cmd += ' -x '
+            cmd += ' ' + self.__replaceCommands['-x'] + ' '
 
         if sPSKc is not None:
             cmd += '0410'
@@ -2574,7 +2624,7 @@
 
         if len(TLVs) != 0:
             tlvs = ''.join('%02x' % tlv for tlv in TLVs)
-            cmd += ' -x '
+            cmd += ' ' + self.__replaceCommands['-x'] + ' '
             cmd += tlvs
 
         return self.__executeCommand(cmd)[-1] == 'Done'
@@ -2623,7 +2673,7 @@
             cmd += str(xPanId)
 
         if xMasterKey is not None:
-            cmd += ' networkkey '
+            cmd += ' ' + self.__replaceCommands['networkkey'] + ' '
             key = self.__convertLongToHex(xMasterKey, 32)
 
             cmd += key
@@ -2637,7 +2687,7 @@
             cmd += self._deviceEscapeEscapable(str(sNetworkName))
 
         if xCommissionerSessionId is not None:
-            cmd += ' -x '
+            cmd += ' ' + self.__replaceCommands['-x'] + ' '
             cmd += '0b02'
             sessionid = str(hex(xCommissionerSessionId))[2:]
 
@@ -2660,7 +2710,7 @@
 
         if len(TLVs) != 0:
             tlvs = ''.join('%02x' % tlv for tlv in TLVs)
-            cmd += ' -x '
+            cmd += ' ' + self.__replaceCommands['-x'] + ' '
             cmd += tlvs
 
         return self.__executeCommand(cmd)[-1] == 'Done'
@@ -2704,22 +2754,12 @@
             cmd += str(hex(xBorderRouterLocator))
 
         if xChannelTlv is not None:
-            cmd += ' -x '
+            cmd += ' ' + self.__replaceCommands['-x'] + ' '
             cmd += '000300' + '%04x' % xChannelTlv
 
         return self.__executeCommand(cmd)[-1] == 'Done'
 
     @API
-    def setActiveDataset(self, listActiveDataset=()):
-        # Unused by the scripts
-        pass
-
-    @API
-    def setCommisionerMode(self):
-        # Unused by the scripts
-        pass
-
-    @API
     def setPSKc(self, strPSKc):
         cmd = 'dataset pskc %s' % strPSKc
         self.hasActiveDatasetToCommit = True
@@ -2833,7 +2873,7 @@
                 self.bbrSeqNum = 128
             else:
                 self.bbrSeqNum = (self.bbrSeqNum + 1) % 256
-        else:
+        elif SeqNum is not None:
             self.bbrSeqNum = SeqNum
 
         return self.__configBbrDataset(SeqNum=self.bbrSeqNum, MlrTimeout=MlrTimeout, ReRegDelay=ReRegDelay)
@@ -2860,7 +2900,8 @@
         if ReRegDelay is not None:
             self.bbrReRegDelay = ReRegDelay
 
-        self.__executeCommand('netdata register')
+        cmd = self.__replaceCommands['netdata register']
+        self.__executeCommand(cmd)
 
         return ret
 
@@ -3176,7 +3217,7 @@
     @watched
     def isBorderRoutingEnabled(self):
         try:
-            self.__executeCommand('br omrprefix')
+            self.__executeCommand('br omrprefix local')
             return True
         except CommandError:
             return False
@@ -3191,6 +3232,31 @@
         except CommandError:
             self._lineSepX = LINESEPX
 
+    def __detectReference20200818(self):
+        """Detect if the device is a Thread reference 20200818 """
+
+        # Running `version api` in Thread reference 20200818 is equivalent to running `version`
+        # It will not output an API number
+        self.IsReference20200818 = not self.__executeCommand('version api')[0].isdigit()
+
+        if self.IsReference20200818:
+            self.__replaceCommands = {
+                '-x': 'binary',
+                'allowlist': 'whitelist',
+                'denylist': 'blacklist',
+                'netdata register': 'netdataregister',
+                'networkkey': 'masterkey',
+                'partitionid preferred': 'leaderpartitionid',
+            }
+        else:
+
+            class IdentityDict:
+
+                def __getitem__(self, key):
+                    return key
+
+            self.__replaceCommands = IdentityDict()
+
     def __discoverDeviceCapability(self):
         """Discover device capability according to version"""
         thver = self.__executeCommand('thread version')[0]
@@ -3238,6 +3304,16 @@
     def setVrCheckSkip(self):
         self.__executeCommand("tvcheck disable")
 
+    @API
+    def addBlockedNodeId(self, node_id):
+        cmd = 'nodeidfilter deny %d' % node_id
+        self.__executeCommand(cmd)
+
+    @API
+    def clearBlockedNodeIds(self):
+        cmd = 'nodeidfilter clear'
+        self.__executeCommand(cmd)
+
 
 class OpenThread(OpenThreadTHCI, IThci):
 
@@ -3274,18 +3350,6 @@
             self.__handle.close()
             self.__handle = None
 
-    def _deviceBeforeReset(self):
-        pass
-
-    def _deviceAfterReset(self):
-        pass
-
-    def __restartAgentService(self):
-        pass
-
-    def _beforeRegisterMulticast(self, sAddr, timeout):
-        pass
-
     def __socRead(self, size=512):
         if self._is_net:
             return self.__handle.recv(size)
@@ -3323,9 +3387,3 @@
             self.__socWrite(line + '\r')
         else:
             self.__socWrite(line + '\r\n')
-
-    def _onCommissionStart(self):
-        pass
-
-    def _onCommissionStop(self):
-        pass
diff --git a/tools/harness-thci/OpenThread_BR.py b/tools/harness-thci/OpenThread_BR.py
index 160d576..83e16bf 100644
--- a/tools/harness-thci/OpenThread_BR.py
+++ b/tools/harness-thci/OpenThread_BR.py
@@ -60,6 +60,8 @@
 
 
 class SSHHandle(object):
+    # Unit: second
+    KEEPALIVE_INTERVAL = 30
 
     def __init__(self, ip, port, username, password):
         self.ip = ip
@@ -85,6 +87,9 @@
             else:
                 raise
 
+        # Avoid SSH disconnection after idle for a long time
+        self.__handle.get_transport().set_keepalive(self.KEEPALIVE_INTERVAL)
+
     def close(self):
         if self.__handle is not None:
             self.__handle.close()
@@ -293,17 +298,21 @@
     IsBorderRouter = True
     __is_root = False
 
+    def _getHandle(self):
+        if self.connectType == 'ip':
+            return SSHHandle(self.telnetIp, self.telnetPort, self.telnetUsername, self.telnetPassword)
+        else:
+            return SerialHandle(self.port, 115200)
+
     def _connect(self):
         self.log("logging in to Raspberry Pi ...")
         self.__cli_output_lines = []
         self.__syslog_skip_lines = None
         self.__syslog_last_read_ts = 0
 
+        self.__handle = self._getHandle()
         if self.connectType == 'ip':
-            self.__handle = SSHHandle(self.telnetIp, self.telnetPort, self.telnetUsername, self.telnetPassword)
             self.__is_root = self.telnetUsername == 'root'
-        else:
-            self.__handle = SerialHandle(self.port, 115200)
 
     def _disconnect(self):
         if self.__handle:
@@ -326,7 +335,7 @@
         self.__truncateSyslog()
         self.__enableAcceptRa()
         if not self.IsHost:
-            self.__restartAgentService()
+            self._restartAgentService()
             time.sleep(2)
 
     def __enableAcceptRa(self):
@@ -582,7 +591,7 @@
         cmd = 'sh -c "cat >/etc/radvd.conf <<%s"' % conf
 
         self.bash(cmd)
-        self.bash('service radvd restart')
+        self.bash(self.extraParams.get('cmd-restart-radvd', 'service radvd restart'))
         self.bash('service radvd status')
 
     @watched
@@ -619,7 +628,7 @@
         for line in output:
             self.__cli_output_lines.append(line)
 
-    def __restartAgentService(self):
+    def _restartAgentService(self):
         restart_cmd = self.extraParams.get('cmd-restart-otbr-agent', 'systemctl restart otbr-agent')
         self.bash(restart_cmd)
 
@@ -627,28 +636,50 @@
         self.bash('truncate -s 0 /var/log/syslog')
 
     def __dumpSyslog(self):
-        output = self.bash_unwatched('grep "otbr-agent" /var/log/syslog')
+        cmd = self.extraParams.get('cmd-dump-otbr-log', 'grep "otbr-agent" /var/log/syslog')
+        output = self.bash_unwatched(cmd)
         for line in output:
             self.log('%s', line)
 
     @API
-    def mdns_query(self, dst='ff02::fb', service='_meshcop._udp.local', addrs_blacklist=[]):
+    def get_eth_addrs(self):
+        cmd = "ip -6 addr list dev %s | grep 'inet6 ' | awk '{print $2}'" % self.backboneNetif
+        addrs = self.bash(cmd)
+        return [addr.split('/')[0] for addr in addrs]
+
+    @API
+    def mdns_query(self, service='_meshcop._udp.local', addrs_allowlist=(), addrs_denylist=()):
+        try:
+            for deny_addr in addrs_denylist:
+                self.bash('ip6tables -A INPUT -p udp --dport 5353 -s %s -j DROP' % deny_addr)
+
+            if addrs_allowlist:
+                for allow_addr in addrs_allowlist:
+                    self.bash('ip6tables -A INPUT -p udp --dport 5353 -s %s -j ACCEPT' % allow_addr)
+
+                self.bash('ip6tables -A INPUT -p udp --dport 5353 -j DROP')
+
+            return self._mdns_query_impl(service, find_active=(addrs_allowlist or addrs_denylist))
+
+        finally:
+            self.bash('ip6tables -F INPUT')
+            time.sleep(1)
+
+    def _mdns_query_impl(self, service, find_active):
         # For BBR-TC-03 or DH test cases (empty arguments) just send a query
-        if dst == 'ff02::fb' and not addrs_blacklist:
-            self.bash('dig -p 5353 @%s %s ptr' % (dst, service), sudo=False)
+        output = self.bash('python3 ~/repo/openthread/tests/scripts/thread-cert/find_border_agents.py')
+
+        if not find_active:
             return
 
         # For MATN-TC-17 and MATN-TC-18 use Zeroconf to get the BBR address and border agent port
-        cmd = 'python3 ~/repo/openthread/tests/scripts/thread-cert/find_border_agents.py'
-        output = self.bash(cmd)
         for line in output:
             print(line)
             alias, addr, port, thread_status = eval(line)
             if thread_status == 2 and addr:
-                if (dst and addr in dst) or (addr not in addrs_blacklist):
-                    if ipaddress.IPv6Address(addr.decode()).is_link_local:
-                        addr = '%s%%%s' % (addr, self.backboneNetif)
-                    return addr, port
+                if ipaddress.IPv6Address(addr.decode()).is_link_local:
+                    addr = '%s%%%s' % (addr, self.backboneNetif)
+                return addr, port
 
         raise Exception('No active Border Agents found')
 
diff --git a/.lgtm.yml b/tools/ot-fct/CMakeLists.txt
similarity index 71%
copy from .lgtm.yml
copy to tools/ot-fct/CMakeLists.txt
index 9051e95..aea348f 100644
--- a/.lgtm.yml
+++ b/tools/ot-fct/CMakeLists.txt
@@ -1,5 +1,5 @@
 #
-#  Copyright (c) 2020, The OpenThread Authors.
+#  Copyright (c) 2022, The OpenThread Authors.
 #  All rights reserved.
 #
 #  Redistribution and use in source and binary forms, with or without
@@ -26,12 +26,24 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-extraction:
-  cpp:
-    prepare:
-      packages:
-        - cmake
-        - ninja-build
-    index:
-      build_command:
-        - THREAD_VERSION=1.2 NODE_MODE=rcp OT_NATIVE_IP=1 ./script/test build
+project(ot-fct)
+
+set(OPENTHREAD_DIR ${PROJECT_SOURCE_DIR}/../../)
+
+include_directories(
+    ${OPENTHREAD_DIR}/include
+    ${OPENTHREAD_DIR}/src
+    ${OPENTHREAD_DIR}/src/core
+    ${OPENTHREAD_DIR}/src/posix/platform
+)
+ 
+add_executable(ot-fct
+    cli.cpp
+    main.cpp
+    logging.cpp
+    ${OPENTHREAD_DIR}/src/core/common/string.cpp
+    ${OPENTHREAD_DIR}/src/core/utils/parse_cmdline.cpp
+    ${OPENTHREAD_DIR}/src/lib/platform/exit_code.c
+    ${OPENTHREAD_DIR}/src/posix/platform/power.cpp
+    ${OPENTHREAD_DIR}/src/posix/platform/config_file.cpp
+)
diff --git a/tools/ot-fct/README.md b/tools/ot-fct/README.md
new file mode 100644
index 0000000..d006dc6
--- /dev/null
+++ b/tools/ot-fct/README.md
@@ -0,0 +1,75 @@
+# OpenThread Factory Tool Reference
+
+## Overview
+
+The ot-fct is used to store the power calibration table into the factory configuration file and show the power related tables.
+
+## Command List
+
+- [powercalibrationtable](#powercalibrationtable)
+- [regiondomaintable](#regiondomaintable)
+- [targetpowertable](#targetpowertable)
+
+#### powercalibrationtable
+
+Show the power calibration table.
+
+```bash
+> powercalibrationtable
+| ChStart |  ChEnd  | ActualPower(0.01dBm) | RawPowerSetting |
++---------+---------+----------------------+-----------------+
+| 11      | 25      | 1900                 | 112233          |
+| 11      | 25      | 1000                 | 223344          |
+| 26      | 26      | 1500                 | 334455          |
+| 26      | 26      | 700                  | 445566          |
+Done
+```
+
+#### powercalibrationtable add -b \<channelstart\>,\<channelend\> -c \<actualpower\>,\<rawpowersetting\>/... ...
+
+Add power calibration table entry.
+
+- channelstart: Sub-band start channel.
+- channelend: Sub-band end channel.
+- actualpower: The actual power in 0.01 dBm.
+- rawpowersetting: The raw power setting hex string.
+
+```bash
+> powercalibrationtable add -b 11,25 -c 1900,112233/1000,223344  -b 26,26 -c 1500,334455/700,445566
+Done
+```
+
+#### powercalibrationtable clear
+
+Clear the power calibration table.
+
+```bash
+> powercalibrationtable clear
+Done
+```
+
+#### regiondomaintable
+
+Show the region and regulatory domain mapping table.
+
+```bash
+> regiondomaintable
+FCC,AU,CA,CL,CO,IN,MX,PE,TW,US
+ETSI,WW
+Done
+```
+
+#### targetpowertable
+
+Show the target power table.
+
+```bash
+> targetpowertable
+|  Domain  | ChStart |  ChEnd  | TargetPower(0.01dBm) |
++----------+---------+---------+----------------------+
+| FCC      | 11      | 14      | 1700                 |
+| FCC      | 15      | 24      | 2000                 |
+| FCC      | 25      | 26      | 1600                 |
+| ETSI     | 11      | 26      | 1000                 |
+Done
+```
diff --git a/tools/ot-fct/cli.cpp b/tools/ot-fct/cli.cpp
new file mode 100644
index 0000000..893e595
--- /dev/null
+++ b/tools/ot-fct/cli.cpp
@@ -0,0 +1,329 @@
+/*
+ *  Copyright (c) 2022, 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 strain 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 "cli.hpp"
+
+#include <assert.h>
+#include <errno.h>
+#include <getopt.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "power.hpp"
+#include "common/code_utils.hpp"
+
+namespace ot {
+namespace Fct {
+
+const struct Cli::Command Cli::sCommands[] = {
+    {"powercalibrationtable", &Cli::ProcessCalibrationTable},
+    {"targetpowertable", &Cli::ProcessTargetPowerTable},
+    {"regiondomaintable", &Cli::ProcessRegionDomainTable},
+};
+
+otError Cli::GetNextTargetPower(const Power::Domain &aDomain, int &aIterator, Power::TargetPower &aTargetPower)
+{
+    otError error = OT_ERROR_NOT_FOUND;
+    char    value[kMaxValueSize];
+    char   *domain;
+    char   *psave;
+
+    while (mProductConfigFile.Get(kKeyTargetPower, aIterator, value, sizeof(value)) == OT_ERROR_NONE)
+    {
+        if (((domain = strtok_r(value, kCommaDelimiter, &psave)) == nullptr) || (aDomain != domain))
+        {
+            continue;
+        }
+
+        error = aTargetPower.FromString(psave);
+        break;
+    }
+
+    return error;
+}
+
+otError Cli::GetNextDomain(int &aIterator, Power::Domain &aDomain)
+{
+    otError error = OT_ERROR_NOT_FOUND;
+    char    value[kMaxValueSize];
+    char   *str;
+
+    while (mProductConfigFile.Get(kKeyRegionDomainMapping, aIterator, value, sizeof(value)) == OT_ERROR_NONE)
+    {
+        if ((str = strtok(value, kCommaDelimiter)) == nullptr)
+        {
+            continue;
+        }
+
+        error = aDomain.Set(str);
+        break;
+    }
+
+exit:
+    return error;
+}
+
+otError Cli::ProcessTargetPowerTable(Utils::CmdLineParser::Arg aArgs[])
+{
+    otError       error    = OT_ERROR_NONE;
+    int           iterator = 0;
+    Power::Domain domain;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    printf("|  Domain  | ChStart |  ChEnd  | TargetPower(0.01dBm) |\r\n");
+    printf("+----------+---------+---------+----------------------+\r\n");
+    while (GetNextDomain(iterator, domain) == OT_ERROR_NONE)
+    {
+        int                iter = 0;
+        Power::TargetPower targetPower;
+
+        while (GetNextTargetPower(domain, iter, targetPower) == OT_ERROR_NONE)
+        {
+            printf("| %-8s | %-7d | %-7d | %-20d |\r\n", domain.AsCString(), targetPower.GetChannelStart(),
+                   targetPower.GetChannelEnd(), targetPower.GetTargetPower());
+        }
+    }
+
+exit:
+    return error;
+}
+
+otError Cli::ProcessRegionDomainTable(Utils::CmdLineParser::Arg aArgs[])
+{
+    otError error    = OT_ERROR_NONE;
+    int     iterator = 0;
+    char    value[kMaxValueSize];
+    char   *domain;
+    char   *psave;
+
+    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+    while (mProductConfigFile.Get(kKeyRegionDomainMapping, iterator, value, sizeof(value)) == OT_ERROR_NONE)
+    {
+        printf("%s\r\n", value);
+    }
+
+exit:
+    return error;
+}
+
+otError Cli::ParseNextCalibratedPower(char                   *aCalibratedPowerString,
+                                      uint16_t                aLength,
+                                      uint16_t               &aIterator,
+                                      Power::CalibratedPower &aCalibratedPower)
+{
+    otError                    error = OT_ERROR_NONE;
+    char                      *start = aCalibratedPowerString + aIterator;
+    char                      *end;
+    char                      *subString;
+    int16_t                    actualPower;
+    ot::Power::RawPowerSetting rawPowerSetting;
+
+    VerifyOrExit(aIterator < aLength, error = OT_ERROR_PARSE);
+
+    end = strstr(start, "/");
+    if (end != nullptr)
+    {
+        aIterator = end - aCalibratedPowerString + 1; // +1 to skip '/'
+        *end      = '\0';
+    }
+    else
+    {
+        aIterator = aLength;
+        end       = aCalibratedPowerString + aLength;
+    }
+
+    subString = strstr(start, kCommaDelimiter);
+    VerifyOrExit(subString != nullptr, error = OT_ERROR_PARSE);
+    *subString = '\0';
+    subString++;
+
+    SuccessOrExit(error = Utils::CmdLineParser::ParseAsInt16(start, actualPower));
+    aCalibratedPower.SetActualPower(actualPower);
+
+    VerifyOrExit(subString < end, error = OT_ERROR_PARSE);
+    SuccessOrExit(error = rawPowerSetting.Set(subString));
+    aCalibratedPower.SetRawPowerSetting(rawPowerSetting);
+
+exit:
+    return error;
+}
+
+otError Cli::ProcessCalibrationTable(Utils::CmdLineParser::Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    if (aArgs[0].IsEmpty())
+    {
+        int  iterator = 0;
+        char value[kMaxValueSize];
+
+        ot::Power::CalibratedPower calibratedPower;
+
+        printf("| ChStart |  ChEnd  | ActualPower(0.01dBm) | RawPowerSetting |\r\n");
+        printf("+---------+---------+----------------------+-----------------+\r\n");
+
+        while (mFactoryConfigFile.Get(kKeyCalibratedPower, iterator, value, sizeof(value)) == OT_ERROR_NONE)
+        {
+            SuccessOrExit(error = calibratedPower.FromString(value));
+            printf("| %-7d | %-7d | %-20d | %-15s |\r\n", calibratedPower.GetChannelStart(),
+                   calibratedPower.GetChannelEnd(), calibratedPower.GetActualPower(),
+                   calibratedPower.GetRawPowerSetting().ToString().AsCString());
+        }
+    }
+    else if (aArgs[0] == "add")
+    {
+        constexpr uint16_t kStateSearchDomain = 0;
+        constexpr uint16_t kStateSearchPower  = 1;
+
+        uint8_t                state = kStateSearchDomain;
+        char                  *subString;
+        uint8_t                channel;
+        Power::CalibratedPower calibratedPower;
+
+        for (Utils::CmdLineParser::Arg *arg = &aArgs[1]; !arg->IsEmpty(); arg++)
+        {
+            if ((state == kStateSearchDomain) && (*arg == "-b"))
+            {
+                arg++;
+                VerifyOrExit(!arg->IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+                subString = strtok(arg->GetCString(), kCommaDelimiter);
+                VerifyOrExit(subString != nullptr, error = OT_ERROR_PARSE);
+                SuccessOrExit(error = Utils::CmdLineParser::ParseAsUint8(subString, channel));
+                calibratedPower.SetChannelStart(channel);
+
+                subString = strtok(NULL, kCommaDelimiter);
+                VerifyOrExit(subString != nullptr, error = OT_ERROR_PARSE);
+                SuccessOrExit(error = Utils::CmdLineParser::ParseAsUint8(subString, channel));
+                calibratedPower.SetChannelEnd(channel);
+                VerifyOrExit(calibratedPower.GetChannelStart() <= calibratedPower.GetChannelEnd(),
+                             error = OT_ERROR_INVALID_ARGS);
+
+                state = kStateSearchPower;
+            }
+            else if ((state == kStateSearchPower) && (*arg == "-c"))
+            {
+                uint16_t length;
+                uint16_t iterator = 0;
+
+                arg++;
+                VerifyOrExit(!arg->IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+
+                length = strlen(arg->GetCString());
+                while (ParseNextCalibratedPower(arg->GetCString(), length, iterator, calibratedPower) == OT_ERROR_NONE)
+                {
+                    SuccessOrExit(
+                        error = mFactoryConfigFile.Add(kKeyCalibratedPower, calibratedPower.ToString().AsCString()));
+                }
+
+                state = kStateSearchDomain;
+            }
+            else
+            {
+                error = OT_ERROR_INVALID_ARGS;
+                break;
+            }
+        }
+
+        if (state == kStateSearchPower)
+        {
+            error = OT_ERROR_INVALID_ARGS;
+        }
+    }
+    else if (aArgs[0] == "clear")
+    {
+        error = mFactoryConfigFile.Clear(kKeyCalibratedPower);
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_ARGS;
+    }
+
+exit:
+    return error;
+}
+
+void Cli::ProcessCommand(Utils::CmdLineParser::Arg aArgs[])
+{
+    otError error = OT_ERROR_NOT_FOUND;
+    int     i;
+
+    for (i = 0; i < (sizeof(sCommands) / sizeof(sCommands[0])); i++)
+    {
+        if (strcmp(aArgs[0].GetCString(), sCommands[i].mName) == 0)
+        {
+            error = (this->*sCommands[i].mCommand)(aArgs + 1);
+            break;
+        }
+    }
+
+exit:
+    AppendErrorResult(error);
+}
+
+void Cli::ProcessLine(char *aLine)
+{
+    const int                 kMaxArgs = 20;
+    Utils::CmdLineParser::Arg args[kMaxArgs + 1];
+
+    SuccessOrExit(ot::Utils::CmdLineParser::ParseCmd(aLine, args, kMaxArgs));
+    VerifyOrExit(!args[0].IsEmpty());
+
+    ProcessCommand(args);
+
+exit:
+    OutputPrompt();
+}
+
+void Cli::OutputPrompt(void)
+{
+    printf("> ");
+    fflush(stdout);
+}
+
+void Cli::AppendErrorResult(otError aError)
+{
+    if (aError != OT_ERROR_NONE)
+    {
+        printf("failed\r\nstatus %#x\r\n", aError);
+    }
+    else
+    {
+        printf("Done\r\n");
+    }
+
+    fflush(stdout);
+}
+} // namespace Fct
+} // namespace ot
diff --git a/tools/ot-fct/cli.hpp b/tools/ot-fct/cli.hpp
new file mode 100644
index 0000000..e679ef5
--- /dev/null
+++ b/tools/ot-fct/cli.hpp
@@ -0,0 +1,117 @@
+/*
+ *  Copyright (c) 2022, 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 strain 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.
+ */
+
+#ifndef CLI_H
+#define CLI_H
+
+#include "openthread-posix-config.h"
+
+#include <stdint.h>
+#include <stdio.h>
+
+#include "config_file.hpp"
+#include "power.hpp"
+#include "utils/parse_cmdline.hpp"
+
+#include <openthread/error.h>
+#include <openthread/platform/radio.h>
+
+namespace ot {
+namespace Fct {
+
+class Cli;
+
+/**
+ * This class implements the factory CLI.
+ *
+ */
+class Cli
+{
+public:
+    Cli(void)
+        : mFactoryConfigFile(OPENTHREAD_POSIX_CONFIG_FACTORY_CONFIG_FILE)
+        , mProductConfigFile(OPENTHREAD_POSIX_CONFIG_PRODUCT_CONFIG_FILE)
+    {
+    }
+
+    /**
+     * This method processes a factory command.
+     *
+     * @param[in]   aArgs          The arguments of command line.
+     * @param[in]   aArgsLength    The number of args in @p aArgs.
+     *
+     */
+    void ProcessCommand(Utils::CmdLineParser::Arg aArgs[]);
+
+    /**
+     * This method processes the command line.
+     *
+     * @param[in]  aLine   A pointer to a command line string.
+     *
+     */
+    void ProcessLine(char *aLine);
+
+    /**
+     * This method outputs the prompt.
+     *
+     */
+    void OutputPrompt(void);
+
+private:
+    static constexpr uint16_t kMaxValueSize           = 512;
+    const char               *kKeyCalibratedPower     = "calibrated_power";
+    const char               *kKeyTargetPower         = "target_power";
+    const char               *kKeyRegionDomainMapping = "region_domain_mapping";
+    const char               *kCommaDelimiter         = ",";
+
+    struct Command
+    {
+        const char *mName;
+        otError (Cli::*mCommand)(Utils::CmdLineParser::Arg aArgs[]);
+    };
+
+    otError ParseNextCalibratedPower(char                   *aCalibratedPowerString,
+                                     uint16_t                aLength,
+                                     uint16_t               &aIterator,
+                                     Power::CalibratedPower &aCalibratedPower);
+    otError ProcessCalibrationTable(Utils::CmdLineParser::Arg aArgs[]);
+    otError ProcessTargetPowerTable(Utils::CmdLineParser::Arg aArgs[]);
+    otError ProcessRegionDomainTable(Utils::CmdLineParser::Arg aArgs[]);
+    otError GetNextDomain(int &aIterator, Power::Domain &aDomain);
+    otError GetNextTargetPower(const Power::Domain &aDomain, int &aIterator, Power::TargetPower &aTargetPower);
+
+    void AppendErrorResult(otError aError);
+
+    static const struct Command sCommands[];
+
+    ot::Posix::ConfigFile mFactoryConfigFile;
+    ot::Posix::ConfigFile mProductConfigFile;
+};
+} // namespace Fct
+} // namespace ot
+#endif
diff --git a/examples/platforms/cc2538/openthread-core-cc2538-config-check.h b/tools/ot-fct/logging.cpp
similarity index 77%
rename from examples/platforms/cc2538/openthread-core-cc2538-config-check.h
rename to tools/ot-fct/logging.cpp
index 93788b1..344b162 100644
--- a/examples/platforms/cc2538/openthread-core-cc2538-config-check.h
+++ b/tools/ot-fct/logging.cpp
@@ -1,10 +1,10 @@
 /*
- *  Copyright (c) 2019, The OpenThread Authors.
+ *  Copyright (c) 2022, 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
+ *  1. Redistributions of source code must strain 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
@@ -26,11 +26,16 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef OPENTHREAD_CORE_CC2538_CONFIG_CHECK_H_
-#define OPENTHREAD_CORE_CC2538_CONFIG_CHECK_H_
+#include <stdio.h>
+#include <stdlib.h>
 
-#if OPENTHREAD_CONFIG_RADIO_915MHZ_OQPSK_SUPPORT
-#error "Platform cc2538 doesn't support configuration option: OPENTHREAD_CONFIG_RADIO_915MHZ_OQPSK_SUPPORT"
-#endif
+#include <openthread/logging.h>
 
-#endif /* OPENTHREAD_CORE_CC2538_CONFIG_CHECK_H_ */
+void otLogCritPlat(const char *aFormat, ...)
+{
+    va_list args;
+
+    va_start(args, aFormat);
+    vprintf(aFormat, args);
+    va_end(args);
+}
diff --git a/tools/ot-fct/main.cpp b/tools/ot-fct/main.cpp
new file mode 100644
index 0000000..ffa98f3
--- /dev/null
+++ b/tools/ot-fct/main.cpp
@@ -0,0 +1,102 @@
+/*
+ *  Copyright (c) 2022, 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 strain 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 <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/select.h>
+#include <unistd.h>
+
+#include "cli.hpp"
+
+static ot::Fct::Cli sCli;
+
+int main(int argc, char *argv[])
+{
+    if (argc >= 2)
+    {
+        const int                     kMaxArgs = 20;
+        ot::Utils::CmdLineParser::Arg args[kMaxArgs + 1];
+
+        if (argc - 1 > kMaxArgs)
+        {
+            fprintf(stderr, "Too many arguments!\r\n");
+            exit(EXIT_FAILURE);
+        }
+
+        for (int i = 0; i < argc - 1; i++)
+        {
+            args[i].SetCString(argv[i + 1]);
+        }
+        args[argc - 1].Clear();
+
+        sCli.ProcessCommand(args);
+    }
+    else
+    {
+        fd_set rset;
+        int    maxFd;
+        int    ret;
+
+        sCli.OutputPrompt();
+
+        while (true)
+        {
+            FD_ZERO(&rset);
+            FD_SET(STDIN_FILENO, &rset);
+            maxFd = STDIN_FILENO + 1;
+
+            ret = select(maxFd, &rset, nullptr, nullptr, nullptr);
+
+            if ((ret == -1) && (errno != EINTR))
+            {
+                fprintf(stderr, "select: %s\n", strerror(errno));
+                break;
+            }
+            else if (ret > 0)
+            {
+                if (FD_ISSET(STDIN_FILENO, &rset))
+                {
+                    const int kBufferSize = 512;
+                    char      buffer[kBufferSize];
+
+                    if (fgets(buffer, sizeof(buffer), stdin) != nullptr)
+                    {
+                        sCli.ProcessLine(buffer);
+                    }
+                    else
+                    {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    return EXIT_SUCCESS;
+}
diff --git a/tools/otci/otci/__init__.py b/tools/otci/otci/__init__.py
index 3e97e89..7cb0aa4 100644
--- a/tools/otci/otci/__init__.py
+++ b/tools/otci/otci/__init__.py
@@ -29,13 +29,16 @@
 
 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, \
     connect_cli_serial, \
     connect_ncp_sim, \
     connect_cmd_handler, \
-    connect_otbr_ssh
+    connect_otbr_ssh, \
+    connect_otbr_adb
+
 from .types import Rloc16, ChildId, NetifIdentifier
 
 _connectors = [
@@ -43,8 +46,17 @@
     'connect_cli_serial',
     'connect_ncp_sim',
     'connect_otbr_ssh',
+    'connect_otbr_adb',
     '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 0055b1a..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
 
@@ -282,3 +281,46 @@
 
     def set_line_read_callback(self, callback: Optional[Callable[[str], Any]]):
         self.__line_read_callback = callback
+
+
+class OtbrAdbCommandRunner(OTCommandHandler):
+
+    def __init__(self, host, port):
+        from adb_shell.adb_device import AdbDeviceTcp
+
+        self.__host = host
+        self.__port = port
+        self.__adb = AdbDeviceTcp(host, port, default_transport_timeout_s=9.0)
+
+        self.__line_read_callback = None
+        self.__adb.connect(rsa_keys=None, auth_timeout_s=0.1)
+
+    def __repr__(self):
+        return f'{self.__host}:{self.__port}'
+
+    def execute_command(self, cmd: str, timeout: float) -> List[str]:
+        sh_cmd = f'ot-ctl {cmd}'
+
+        output = self.shell(sh_cmd, timeout=timeout)
+
+        if self.__line_read_callback is not None:
+            for line in output:
+                self.__line_read_callback(line)
+
+        if cmd in ('reset', 'factoryreset'):
+            self.wait(3)
+
+        return output
+
+    def shell(self, cmd: str, timeout: float) -> List[str]:
+        return self.__adb.shell(cmd, timeout_s=timeout).splitlines()
+
+    def close(self):
+        self.__adb.close()
+
+    def wait(self, duration: float) -> List[str]:
+        time.sleep(duration)
+        return []
+
+    def set_line_read_callback(self, callback: Optional[Callable[[str], Any]]):
+        self.__line_read_callback = callback
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 da6ef61..9a8c2cb 100644
--- a/tools/otci/otci/otci.py
+++ b/tools/otci/otci/otci.py
@@ -33,7 +33,7 @@
 from typing import Callable, List, Collection, Union, Tuple, Optional, Dict, Pattern, Any
 
 from . import connectors
-from .command_handlers import OTCommandHandler, OtCliCommandRunner, OtbrSshCommandRunner
+from .command_handlers import OTCommandHandler, OtCliCommandRunner, OtbrSshCommandRunner, OtbrAdbCommandRunner
 from .connectors import Simulator
 from .errors import UnexpectedCommandOutput, ExpectLineTimeoutError, CommandError, InvalidArgumentsError
 from .types import ChildId, Rloc16, Ip6Addr, ThreadState, PartitionId, DeviceMode, RouterId, SecurityPolicy, Ip6Prefix, \
@@ -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.
@@ -101,8 +101,10 @@
             try:
                 return self.__execute_command(cmd, timeout, silent, already_is_ok=already_is_ok)
             except Exception:
+                self.wait(2)
                 if i == self.__exec_command_retry:
                     raise
+        assert False
 
     def __execute_command(self,
                           cmd: str,
@@ -218,7 +220,7 @@
     )
 
     def ping(self,
-             ip: str,
+             ip: Union[str, Ip6Addr],
              size: int = 8,
              count: int = 1,
              interval: float = 1,
@@ -260,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}'
 
@@ -299,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:
@@ -495,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."""
@@ -783,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])
@@ -798,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:
@@ -935,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'):
@@ -961,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'):
@@ -976,7 +982,7 @@
                     info['addresses'] = list(map(Ip6Addr, v.split(', ')))
                 elif k == 'subtypes':
                     info[k] = list() if v == '(null)' else list(v.split(','))
-                elif k in ('port', 'weight', 'priority', 'ttl'):
+                elif k in ('port', 'weight', 'priority', 'ttl', 'lease', 'key-lease'):
                     info[k] = int(v)
                 elif k in ('host',):
                     info[k] = v
@@ -1129,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:
@@ -1163,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:
@@ -1374,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.
@@ -1486,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:
@@ -1569,7 +1581,16 @@
                 routes_output.append(line)
 
         netdata['routes'] = self.__parse_routes(routes_output)
-        netdata['services'] = self.__parse_services(output)
+
+        services_output = []
+        while True:
+            line = output.pop(0)
+            if line == 'Contexts:':
+                break
+            else:
+                services_output.append(line)
+
+        netdata['services'] = self.__parse_services(services_output)
 
         return netdata
 
@@ -1621,9 +1642,9 @@
         routes = []
         for line in output:
             line = line.split()
-            if line[1] == 's':
-                prefix, _, prf, rloc16 = line
-                stable = True
+            if len(line) == 4:
+                prefix, flags, prf, rloc16 = line
+                stable = 's' in flags
             else:
                 prefix, prf, rloc16 = line
                 stable = False
@@ -1770,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}')
 
@@ -1830,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:
@@ -2043,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.
@@ -2207,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.
@@ -2282,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:
@@ -2294,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:
@@ -2302,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:
@@ -2486,5 +2515,10 @@
     return OTCI(cmd_handler)
 
 
+def connect_otbr_adb(host: str, port: int = 5555):
+    cmd_handler = OtbrAdbCommandRunner(host, port)
+    return OTCI(cmd_handler)
+
+
 def connect_cmd_handler(cmd_handler: OTCommandHandler) -> OTCI:
     return OTCI(cmd_handler)
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/otci/setup.py b/tools/otci/setup.py
index f3d6868..6284ace 100644
--- a/tools/otci/setup.py
+++ b/tools/otci/setup.py
@@ -46,5 +46,5 @@
         "Operating System :: OS Independent",
     ],
     python_requires='>=3.6',
-    install_requires=['pySerial', 'paramiko', 'pyspinel'],
+    install_requires=['pySerial', 'paramiko', 'pyspinel', 'adb-shell'],
 )
diff --git a/tools/otci/tests/test_otci.py b/tools/otci/tests/test_otci.py
index 38c5981..ad4b25f 100644
--- a/tools/otci/tests/test_otci.py
+++ b/tools/otci/tests/test_otci.py
@@ -243,7 +243,7 @@
         for counter_name in leader.counter_names:
             logging.info('counter %s: %r', counter_name, leader.get_counter(counter_name))
             leader.reset_counter(counter_name)
-            self.assertTrue(all(x == 0 for x in leader.get_counter(counter_name).values()))
+            self.assertTrue(all(x == 0 for name, x in leader.get_counter(counter_name).items() if "Time" not in name))
 
         logging.info("CSL config: %r", leader.get_csl_config())
         leader.config_csl(channel=13, period=100, timeout=200)
@@ -378,7 +378,7 @@
         self.assertEqual('default.service.arpa.', server.srp_server_get_domain())
 
         default_leases = server.srp_server_get_lease()
-        self.assertEqual(default_leases, (1800, 7200, 86400, 1209600))
+        self.assertEqual(default_leases, (30, 97200, 30, 680400))
         server.srp_server_set_lease(1801, 7201, 86401, 1209601)
         leases = server.srp_server_get_lease()
         self.assertEqual(leases, (1801, 7201, 86401, 1209601))
diff --git a/tools/spi-hdlc-adapter/spi-hdlc-adapter.c b/tools/spi-hdlc-adapter/spi-hdlc-adapter.c
index 76650fc..0adff6b 100644
--- a/tools/spi-hdlc-adapter/spi-hdlc-adapter.c
+++ b/tools/spi-hdlc-adapter/spi-hdlc-adapter.c
@@ -314,9 +314,9 @@
     // This is the last hurah for this process.
     // We dump the stack, because that's all we can do.
 
-    void *      stack_mem[AUTO_PRINT_BACKTRACE_STACK_DEPTH];
-    void **     stack = stack_mem;
-    char **     stack_symbols;
+    void       *stack_mem[AUTO_PRINT_BACKTRACE_STACK_DEPTH];
+    void      **stack = stack_mem;
+    char      **stack_symbols;
     int         stack_depth, i;
     ucontext_t *uc = (ucontext_t *)ucontext;
 
@@ -395,10 +395,7 @@
 /* ------------------------------------------------------------------------- */
 /* MARK: SPI Transfer Functions */
 
-static void spi_header_set_flag_byte(uint8_t *header, uint8_t value)
-{
-    header[0] = value;
-}
+static void spi_header_set_flag_byte(uint8_t *header, uint8_t value) { header[0] = value; }
 
 static void spi_header_set_accept_len(uint8_t *header, uint16_t len)
 {
@@ -412,20 +409,11 @@
     header[4] = ((len >> 8) & 0xFF);
 }
 
-static uint8_t spi_header_get_flag_byte(const uint8_t *header)
-{
-    return header[0];
-}
+static uint8_t spi_header_get_flag_byte(const uint8_t *header) { return header[0]; }
 
-static uint16_t spi_header_get_accept_len(const uint8_t *header)
-{
-    return (header[1] + (uint16_t)(header[2] << 8));
-}
+static uint16_t spi_header_get_accept_len(const uint8_t *header) { return (header[1] + (uint16_t)(header[2] << 8)); }
 
-static uint16_t spi_header_get_data_len(const uint8_t *header)
-{
-    return (header[3] + (uint16_t)(header[4] << 8));
-}
+static uint16_t spi_header_get_data_len(const uint8_t *header) { return (header[3] + (uint16_t)(header[4] << 8)); }
 
 static uint8_t *get_real_rx_frame_start(void)
 {
@@ -813,7 +801,7 @@
 static int push_hdlc(void)
 {
     int             ret              = 0;
-    const uint8_t * spiRxFrameBuffer = get_real_rx_frame_start();
+    const uint8_t  *spiRxFrameBuffer = get_real_rx_frame_start();
     static uint8_t  escaped_frame_buffer[MAX_FRAME_SIZE * 2];
     static uint16_t unescaped_frame_len;
     static uint16_t escaped_frame_len;
@@ -1019,7 +1007,7 @@
 static int push_raw(void)
 {
     int             ret              = 0;
-    const uint8_t * spiRxFrameBuffer = get_real_rx_frame_start();
+    const uint8_t  *spiRxFrameBuffer = get_real_rx_frame_start();
     static uint8_t  raw_frame_buffer[MAX_FRAME_SIZE];
     static uint16_t raw_frame_len;
     static uint16_t raw_frame_sent;
@@ -1291,9 +1279,9 @@
 
 static bool setup_int_gpio(const char *path)
 {
-    char *  edge_path  = NULL;
-    char *  dir_path   = NULL;
-    char *  value_path = NULL;
+    char   *edge_path  = NULL;
+    char   *dir_path   = NULL;
+    char   *value_path = NULL;
     ssize_t len;
     int     setup_fd = -1;
 
diff --git a/.lgtm.yml b/zephyr/module.yml
similarity index 85%
rename from .lgtm.yml
rename to zephyr/module.yml
index 9051e95..75f79ec 100644
--- a/.lgtm.yml
+++ b/zephyr/module.yml
@@ -1,5 +1,5 @@
 #
-#  Copyright (c) 2020, The OpenThread Authors.
+#  Copyright (c) 2022, The OpenThread Authors.
 #  All rights reserved.
 #
 #  Redistribution and use in source and binary forms, with or without
@@ -26,12 +26,6 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-extraction:
-  cpp:
-    prepare:
-      packages:
-        - cmake
-        - ninja-build
-    index:
-      build_command:
-        - THREAD_VERSION=1.2 NODE_MODE=rcp OT_NATIVE_IP=1 ./script/test build
+build:
+  cmake-ext: True
+  kconfig-ext: True